baseline implementation of host-only P25 Phase 2 implementation (this creates the plumbing required later once the SDR modems have support for Phase 2);

pull/128/head
Bryan Biedenkapp 2 months ago
parent 56bff0489b
commit e9dc058704

@ -24,6 +24,7 @@ file(GLOB common_SRC
"src/common/p25/dfsi/frames/*.cpp"
"src/common/p25/dfsi/frames/fsc/*.cpp"
"src/common/p25/lc/*.cpp"
"src/common/p25/lc/mac/*.cpp"
"src/common/p25/lc/tdulc/*.cpp"
"src/common/p25/lc/tsbk/*.cpp"
"src/common/p25/lc/tsbk/mbt/*.cpp"
@ -76,6 +77,7 @@ file(GLOB common_INCLUDE
"src/common/p25/dfsi/frames/*.h"
"src/common/p25/dfsi/frames/fsc/*.h"
"src/common/p25/lc/*.h"
"src/common/p25/lc/mac/*.h"
"src/common/p25/lc/tdulc/*.h"
"src/common/p25/lc/tsbk/*.h"
"src/common/p25/lc/tsbk/mbt/*.h"

@ -0,0 +1,194 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/edac/AMBE2FEC.h"
#include "common/edac/Golay24128.h"
#include "common/Utils.h"
#include <cassert>
#include <cstring>
using namespace edac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Regenerates the P25 Phase 2 half-rate FEC. */
uint32_t AMBE2FEC::regenerate(uint8_t* bytes) const
{
assert(bytes != nullptr);
uint32_t c0 = readBits(bytes, 0U, 24U);
uint32_t c1 = readBits(bytes, 24U, 23U);
uint32_t u0 = 0U;
bool c0Valid = Golay24128::decode24128(c0, u0);
uint32_t corrected = c0Valid ? 0U : 1U;
uint32_t u1 = Golay24128::decode23127(c1 ^ makePN(u0));
uint32_t correctedC1 = (Golay24128::encode23127(u1) >> 1U) ^ makePN(u0);
corrected += Utils::countBits32(c1 ^ correctedC1);
writeBits(bytes, 0U, 24U, Golay24128::encode24128(u0));
writeBits(bytes, 24U, 23U, correctedC1);
return corrected;
}
/* Regenerates both no-sync voice codewords in a Phase 2 4V/2V burst. */
uint32_t AMBE2FEC::regenerateBurst(uint8_t* burst, bool inbound, bool fourVoice) const
{
assert(burst != nullptr);
static const uint32_t inboundOffsets[4U] = {0U, 74U, 174U, 248U};
static const uint32_t outboundOffsets[4U] = {2U, 76U, 176U, 250U};
const uint32_t* offsets = inbound ? inboundOffsets : outboundOffsets;
uint32_t corrected = 0U;
uint32_t frameCount = fourVoice ? 4U : 2U;
for (uint32_t frame = 0U; frame < frameCount; frame++) {
uint8_t transmitted[9U] = {0U};
uint8_t codeword[9U] = {0U};
for (uint32_t bit = 0U; bit < 72U; bit++)
writeBits(transmitted, bit, 1U, readBits(burst, offsets[frame] + bit, 1U));
deinterleave(codeword, transmitted);
corrected += regenerate(codeword);
interleave(transmitted, codeword);
for (uint32_t bit = 0U; bit < 72U; bit++)
writeBits(burst, offsets[frame] + bit, 1U, readBits(transmitted, bit, 1U));
}
return corrected;
}
/* Encodes the prioritized half-rate parameter words. */
void AMBE2FEC::encode(uint8_t* bytes, uint16_t u0, uint16_t u1, uint16_t u2, uint16_t u3) const
{
assert(bytes != nullptr);
::memset(bytes, 0x00U, 9U);
writeBits(bytes, 0U, 24U, Golay24128::encode24128(u0));
writeBits(bytes, 24U, 23U, (Golay24128::encode23127(u1) >> 1U) ^ makePN(u0));
writeBits(bytes, 47U, 11U, u2);
writeBits(bytes, 58U, 14U, u3);
}
/* Converts a deinterleaved codeword to the 36 transmitted dibits. */
void AMBE2FEC::interleave(uint8_t* frame, const uint8_t* codeword) const
{
assert(frame != nullptr);
assert(codeword != nullptr);
::memset(frame, 0x00U, 9U);
for (uint32_t symbol = 0U; symbol < 36U; symbol++) {
uint8_t bit1 = readWordBit(codeword, ANNEX_S[symbol][0U]);
uint8_t bit0 = readWordBit(codeword, ANNEX_S[symbol][1U]);
writeBits(frame, symbol * 2U, 2U, (bit1 << 1U) | bit0);
}
}
/* Converts 36 transmitted dibits to a deinterleaved codeword. */
void AMBE2FEC::deinterleave(uint8_t* codeword, const uint8_t* frame) const
{
assert(codeword != nullptr);
assert(frame != nullptr);
::memset(codeword, 0x00U, 9U);
for (uint32_t symbol = 0U; symbol < 36U; symbol++) {
uint32_t dibit = readBits(frame, symbol * 2U, 2U);
const Ambe2BitRef& ref1 = ANNEX_S[symbol][0U];
const Ambe2BitRef& ref0 = ANNEX_S[symbol][1U];
writeWordBit(codeword, ref1, static_cast<uint8_t>((dibit >> 1U) & 1U));
writeWordBit(codeword, ref0, static_cast<uint8_t>(dibit & 1U));
}
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Returns the bit offset of the specified half-rate word. */
uint32_t AMBE2FEC::wordOffset(uint8_t word)
{
static const uint32_t offsets[4U] = { 0U, 24U, 47U, 58U };
return offsets[word];
}
/* Returns the bit width of the specified half-rate word. */
uint32_t AMBE2FEC::wordWidth(uint8_t word)
{
static const uint32_t widths[4U] = { 24U, 23U, 11U, 14U };
return widths[word];
}
/* Returns the bit reference of the specified half-rate word and bit. */
uint8_t AMBE2FEC::readWordBit(const uint8_t* codeword, const Ambe2BitRef& ref)
{
uint32_t offset = wordOffset(ref.word) + wordWidth(ref.word) - 1U - ref.bit;
return static_cast<uint8_t>((codeword[offset >> 3U] >> (7U - (offset & 7U))) & 1U);
}
/* Writes a bit to the specified half-rate word and bit. */
void AMBE2FEC::writeWordBit(uint8_t* codeword, const Ambe2BitRef& ref, uint8_t bit)
{
uint32_t offset = wordOffset(ref.word) + wordWidth(ref.word) - 1U - ref.bit;
uint8_t mask = static_cast<uint8_t>(1U << (7U - (offset & 7U)));
if (bit != 0U)
codeword[offset >> 3U] |= mask;
else
codeword[offset >> 3U] &= static_cast<uint8_t>(~mask);
}
/* Reads a bit field from a byte array. */
uint32_t AMBE2FEC::readBits(const uint8_t* bytes, uint32_t offset, uint32_t length)
{
uint32_t value = 0U;
for (uint32_t i = 0U; i < length; i++)
value = (value << 1U) | ((bytes[(offset + i) >> 3U] >> (7U - ((offset + i) & 7U))) & 1U);
return value;
}
/* Writes a bit field to a byte array. */
void AMBE2FEC::writeBits(uint8_t* bytes, uint32_t offset, uint32_t length, uint32_t value)
{
for (uint32_t i = 0U; i < length; i++) {
uint32_t bit = (value >> (length - 1U - i)) & 1U;
uint8_t mask = static_cast<uint8_t>(1U << (7U - ((offset + i) & 7U)));
bytes[(offset + i) >> 3U] = bit != 0U ? bytes[(offset + i) >> 3U] | mask :
bytes[(offset + i) >> 3U] & static_cast<uint8_t>(~mask);
}
}
/* Generates a pseudo-random number based on the input value. */
uint32_t AMBE2FEC::makePN(uint32_t u0)
{
uint32_t pn = 0U;
uint32_t state = 16U * u0;
for (uint32_t i = 0U; i < 23U; i++) {
state = (173U * state + 13849U) & 0xFFFFU;
pn = (pn << 1U) | (state >= 32768U ? 1U : 0U);
}
return pn;
}

@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file AMBE2FEC.h
* @ingroup edac
* @file AMBE2FEC.cpp
* @ingroup edac
*/
#if !defined(__AMBE2_FEC_H__)
#define __AMBE2_FEC_H__
#include "common/Defines.h"
namespace edac
{
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Reference to a bit in a P25 Phase 2 half-rate codeword.
* @ingroup edac
*/
struct Ambe2BitRef {
uint8_t word;
uint8_t bit;
};
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
// TIA-102.BABA-A Annex S, symbols 0 through 35.
const Ambe2BitRef ANNEX_S[36U][2U] = {
{{0U,23U},{0U,5U}}, {{1U,10U},{2U,3U}}, {{0U,22U},{0U,4U}}, {{1U,9U},{2U,2U}},
{{0U,21U},{0U,3U}}, {{1U,8U},{2U,1U}}, {{0U,20U},{0U,2U}}, {{1U,7U},{2U,0U}},
{{0U,19U},{0U,1U}}, {{1U,6U},{3U,13U}}, {{0U,18U},{0U,0U}}, {{1U,5U},{3U,12U}},
{{0U,17U},{1U,22U}}, {{1U,4U},{3U,11U}}, {{0U,16U},{1U,21U}}, {{1U,3U},{3U,10U}},
{{0U,15U},{1U,20U}}, {{1U,2U},{3U,9U}}, {{0U,14U},{1U,19U}}, {{1U,1U},{3U,8U}},
{{0U,13U},{1U,18U}}, {{1U,0U},{3U,7U}}, {{0U,12U},{1U,17U}}, {{2U,10U},{3U,6U}},
{{0U,11U},{1U,16U}}, {{2U,9U},{3U,5U}}, {{0U,10U},{1U,15U}}, {{2U,8U},{3U,4U}},
{{0U,9U},{1U,14U}}, {{2U,7U},{3U,3U}}, {{0U,8U},{1U,13U}}, {{2U,6U},{3U,2U}},
{{0U,7U},{1U,12U}}, {{2U,5U},{3U,1U}},
// Annex S source erratum: PDF prints c0(5); c0(6) is required.
{{0U,6U},{1U,11U}}, {{2U,4U},{3U,0U}}
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements P25 Phase 2 half-rate AMBE+2 voice FEC.
* @ingroup edac
*
* The input/output buffer is the 72-bit deinterleaved half-rate codeword:
* c0[24], c1[23], c2[11], c3[14], MSB first. Annex S conversion between
* transmitted dibits and this codeword is intentionally separate.
*/
class HOST_SW_API AMBE2FEC {
public:
/**
* @brief Regenerates the P25 Phase 2 half-rate FEC.
* @param bytes 9-byte, 72-bit deinterleaved codeword.
* @returns uint32_t Number of corrected codeword errors.
*/
uint32_t regenerate(uint8_t* bytes) const;
/**
* @brief Regenerates both no-sync voice codewords in a Phase 2 4V/2V burst.
* @param burst 40-byte host-side burst representation.
* @param inbound True for IEMI/inbound layout, false for OEMI/outbound layout.
* @returns uint32_t Number of corrected codeword errors.
*/
uint32_t regenerateBurst(uint8_t* burst, bool inbound, bool fourVoice) const;
/**
* @brief Encodes the prioritized half-rate parameter words.
* @param[out] bytes 9-byte, 72-bit deinterleaved codeword.
* @param u0 First 12-bit prioritized word.
* @param u1 Second 12-bit prioritized word.
* @param u2 Third 11-bit prioritized word.
* @param u3 Fourth 14-bit prioritized word.
*/
void encode(uint8_t* bytes, uint16_t u0, uint16_t u1, uint16_t u2, uint16_t u3) const;
/**
* @brief Converts a deinterleaved codeword to the 36 transmitted dibits.
* @param[out] frame 9-byte, 72-bit transmitted frame.
* @param codeword 9-byte, 72-bit deinterleaved codeword.
*/
void interleave(uint8_t* frame, const uint8_t* codeword) const;
/**
* @brief Converts 36 transmitted dibits to a deinterleaved codeword.
* @param[out] codeword 9-byte, 72-bit deinterleaved codeword.
* @param frame 9-byte, 72-bit transmitted frame.
*/
void deinterleave(uint8_t* codeword, const uint8_t* frame) const;
private:
/**
* @brief Returns the bit offset of the specified half-rate word.
* @param word Half-rate word index (0-3).
* @returns uint32_t Bit offset of the specified half-rate word.
*/
static uint32_t wordOffset(uint8_t word);
/**
* @brief Returns the bit width of the specified half-rate word.
* @param word Half-rate word index (0-3).
* @returns uint32_t Bit width of the specified half-rate word.
*/
static uint32_t wordWidth(uint8_t word);
/**
* @brief Returns the bit reference of the specified half-rate word and bit.
* @param word Half-rate word index (0-3).
* @param bit Bit index within the specified half-rate word.
* @returns Ambe2BitRef Bit reference of the specified half-rate word and bit.
*/
static uint8_t readWordBit(const uint8_t *codeword, const Ambe2BitRef &ref);
/**
* @brief Writes a bit to the specified half-rate word and bit.
* @param codeword 9-byte, 72-bit deinterleaved codeword.
* @param ref Bit reference of the specified half-rate word and bit.
* @param bit Bit value to write (0 or 1).
*/
static void writeWordBit(uint8_t *codeword, const Ambe2BitRef &ref, uint8_t bit);
/**
* @brief Reads a bit field from a byte array.
* @param bytes Byte array to read from.
* @param offset Bit offset to start reading from.
* @param length Number of bits to read.
* @returns uint32_t Value of the read bit field.
*/
static uint32_t readBits(const uint8_t *bytes, uint32_t offset, uint32_t length);
/**
* @brief Writes a bit field to a byte array.
* @param bytes Byte array to write to.
* @param offset Bit offset to start writing to.
* @param length Number of bits to write.
* @param value Value of the bit field to write.
*/
static void writeBits(uint8_t* bytes, uint32_t offset, uint32_t length, uint32_t value);
/**
* @brief Generates a pseudo-random number based on the input value.
* @param u0 Input value to generate the pseudo-random number from.
* @returns uint32_t Generated pseudo-random number.
*/
static uint32_t makePN(uint32_t u0);
};
}
#endif // __AMBE2_FEC_H__

@ -1676,7 +1676,7 @@ UInt8Array BaseNetwork::createP25P2_Message(uint32_t& length, const p25::lc::LC&
buffer[14U] = controlByte;
buffer[19U] = slot ? 0x00U : 0x80U; // Slot Number
buffer[19U] = slot ? 0x80U : 0x00U; // Slot Number
buffer[19U] |= (uint8_t)duid; // Phase 2 DUID
// pack raw P25 Phase 2 bytes

@ -70,6 +70,11 @@ namespace p25
// TIA-102.BBAC-A Section 4
const uint32_t P25_P2_FRAME_LENGTH_BYTES = 40U;
const uint32_t P25_P2_FRAME_LENGTH_BITS = P25_P2_FRAME_LENGTH_BYTES * 8U;
// Host/modem frames carry a modem tag and explicit DUID followed by the
// 40-byte logical burst. The DUID is transport metadata and is not aired.
const uint32_t P25_P2_HOST_FRAME_LENGTH_BYTES = P25_P2_FRAME_LENGTH_BYTES + 2U;
const uint32_t P25_P2_BURST_LENGTH_BITS = 360U;
const uint32_t P25_P2_SCRAMBLER_SEQUENCE_BITS = 4320U;
const uint32_t P25_P2_IEMI_LENGTH_BITS = 312U;
const uint32_t P25_P2_IEMI_LENGTH_BYTES = (P25_P2_IEMI_LENGTH_BITS / 8U) + 1U;
@ -184,6 +189,10 @@ namespace p25
const uint32_t DEFAULT_SILENCE_THRESHOLD = 124U;
/** @brief Default Frame Loss Threshold */
const uint32_t DEFAULT_FRAME_LOSS_THRESHOLD = 6U;
/** @brief Default Phase 2 Silence Threshold */
const uint32_t P2_DEFAULT_SILENCE_THRESHOLD = 21U;
/** @brief Default Phase 2 Frame Loss Threshold */
const uint32_t P2_DEFAULT_FRAME_LOSS_THRESHOLD = 2U;
/** @brief Maximum P25 voice frame errors */
const uint32_t MAX_P25_VOICE_ERRORS = 1233U;
/** @} */
@ -864,7 +873,7 @@ namespace p25
};
}
// TIA-102.BBAD-D Section 4.1
// TIA-102.BBAD-A Section 2.3.3
/** @brief Phase 2 MAC PDU Opcode(s) */
namespace P2_MAC_HEADER_OPCODE {
/** @brief Phase 2 MAC PDU Opcode(s) */
@ -879,7 +888,7 @@ namespace p25
};
}
// TIA-102.BBAD-D Section 4.2
// TIA-102.BBAD-A Section 2.3.3
/** @brief Phase 2 MAC 4V/SACCH Offset(s) */
namespace P2_MAC_HEADER_OFFSET {
/** @brief Phase 2 MAC 4V/SACCH Offset(s) */
@ -897,7 +906,7 @@ namespace p25
};
}
// TIA-102.BBAD-D Section 3
// TIA-102.BBAD-A Section 3
/** @brief Phase 2 MAC MCO Partitioning */
namespace P2_MAC_MCO_PARTITION {
/** @brief Phase 2 MAC MCO Partitioning */
@ -910,7 +919,7 @@ namespace p25
};
}
// TIA-102.BBAD-D Section 3
// TIA-102.BBAD-A Section 3
/** @brief Phase 2 MAC PDU Opcode(s) */
namespace P2_MAC_MCO {
/** @brief Phase 2 MAC PDU Opcode(s) */
@ -952,9 +961,11 @@ namespace p25
enum E : uint8_t {
VTCH_4V = 0x00U, //!< Inbound/Outbound 4V
SACCH_SCRAMBLED = 0x03U, //!< SACCH Scrambled
LCCH_SCRAMBLED = 0x04U, //!< Inbound LCCH Scrambled
VTCH_2V = 0x06U, //!< Inbound/Outbound 2V
FACCH_SCRAMBLED = 0x09U, //!< FACCH Scrambled
SACCH_UNSCRAMBLED = 0x0CU, //!< SACCH Unscrambled
LCCH_UNSCRAMBLED = 0x0DU, //!< Inbound/Outbound LCCH Unscrambled
FACCH_UNSCRAMBLED = 0x0FU //!< FACCH Unscrambled
};
}

@ -198,7 +198,8 @@ LC::LC() :
m_mi(nullptr),
m_userAlias(nullptr),
m_gotUserAliasPartA(false),
m_gotUserAlias(false)
m_gotUserAlias(false),
m_p2MCODataLength(0U)
{
m_mi = new uint8_t[MI_LENGTH_BYTES];
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
@ -655,7 +656,7 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync)
m_p2DUID = duid[0U] >> 4U;
if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V || !sync)
if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V)
return true; // don't handle 4V or 2V voice PDUs here -- user code will handle
else {
::memset(raw, 0x00U, lengthBytes);
@ -669,14 +670,17 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync)
source = burst;
}
// IEMI with sync: extract data bits (skip 14-bit sync and DUIDs)
// Extract the four information fields, skipping sync (when present) and DUIDs.
for (uint32_t i = 0U; i < lengthBits; i++) {
uint32_t n = i + 14U; // Skip 14-bit sync
if (i >= 36U)
uint32_t n = i + (sync ? 14U : 0U);
uint32_t field1 = sync ? 36U : 72U;
uint32_t field2 = field1 + 72U;
uint32_t field3 = field2 + 96U;
if (i >= field1)
n += 2U; // skip DUID 1 after field 1 (36 bits)
if (i >= 108U)
if (i >= field2)
n += 2U; // skip DUID 2 after field 2 (36+72)
if (i >= 204U)
if (i >= field3)
n += 2U; // skip DUID 3 after field 3 (36+72+96)
bool b = READ_BIT(source, n);
@ -712,6 +716,45 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync)
return true;
}
/* Encode an inbound/IEMI VCH MAC PDU. */
void LC::encodeVCH_MACPDU_IEMI(uint8_t* data, bool sync)
{
assert(data != nullptr);
::memset(data, 0x00U, P25_P2_FRAME_LENGTH_BYTES);
uint32_t lengthBits = sync ? P25_P2_IEMI_WSYNC_LENGTH_BITS : P25_P2_IEMI_LENGTH_BITS;
uint8_t raw[P25_P2_IEMI_LENGTH_BYTES] = { 0U };
if (m_p2DUID != P2_DUID::VTCH_4V && m_p2DUID != P2_DUID::VTCH_2V) {
encodeMACPDU(raw, P25_P2_IEMI_MAC_LENGTH_BITS);
m_rs.encode462621(raw);
for (uint32_t i = 0U; i < lengthBits; i++) {
uint32_t field1 = sync ? 36U : 72U;
uint32_t field2 = field1 + 72U;
uint32_t field3 = field2 + 96U;
uint32_t n = i + (sync ? 14U : 0U);
if (i >= field1) n += 2U;
if (i >= field2) n += 2U;
if (i >= field3) n += 2U;
WRITE_BIT(data, n, READ_BIT(raw, i));
}
}
uint8_t duidRaw[2U] = { static_cast<uint8_t>((m_p2DUID & 0x0FU) << 4U), 0U };
uint8_t duid[2U] = { 0U };
encodeP2_DUIDHamming(duid, duidRaw);
for (uint8_t i = 0U; i < 8U; i++) {
uint32_t n = i + (sync ? 50U : 72U);
if (i >= 2U) n += 72U;
if (i >= 4U) n += 96U;
if (i >= 6U) n += 72U;
WRITE_BIT(data, n, READ_BIT(duid, i));
}
if (m_p2DUID == P2_DUID::FACCH_SCRAMBLED || m_p2DUID == P2_DUID::SACCH_SCRAMBLED)
applyP2Scrambler(data, true, sync);
}
/* Decode a xOEMI VCH MAC PDU. */
bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync)
@ -935,6 +978,16 @@ bool LC::isStandardMFId() const
return false;
}
uint32_t LC::getP2MCOData(uint8_t* data, uint32_t length) const
{
if (data == nullptr || length == 0U || p2MCOData == nullptr)
return 0U;
uint32_t count = length < m_p2MCODataLength ? length : m_p2MCODataLength;
::memcpy(data, p2MCOData, count);
return count;
}
/* Set the Phase 2 scrambler superframe bit offset. */
void LC::setP2ScrambleOffset(uint16_t offset)
@ -1262,7 +1315,7 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength)
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
::memcpy(m_mi, raw + 1U, MI_LENGTH_BYTES); // Message Indicator
m_kId = (raw[10U] << 8) + raw[11U]; // Key ID
m_kId = static_cast<uint16_t>(raw[11U] | (raw[12U] << 8U)); // Key ID
if (!m_encrypted) {
m_encryptOverride = true;
m_encrypted = true;
@ -1333,6 +1386,9 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength)
m_srcId = GET_UINT24(raw, 5U); // Source/Target Address
}
break;
case P2_MAC_MCO::MAC_RELEASE:
m_srcId = GET_UINT24(raw, 3U); // Source Radio Address
break;
case P2_MAC_MCO::PDU_NULL:
break;
@ -1348,11 +1404,9 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength)
delete[] p2MCOData;
uint32_t macLengthBytes = (macLength / 8U) + ((macLength % 8U) ? 1U : 0U);
p2MCOData = new uint8_t[macLengthBytes];
::memset(p2MCOData, 0x00U, macLengthBytes);
// this will include the entire MCO (and depending on message length multiple MCOs)
::memcpy(p2MCOData, raw + 1U, macLengthBytes - 3U); // excluding MAC PDU header and CRC
m_p2MCODataLength = macLengthBytes - 3U;
p2MCOData = new uint8_t[m_p2MCODataLength];
::memcpy(p2MCOData, raw + 1U, m_p2MCODataLength);
}
break;
@ -1599,14 +1653,15 @@ void LC::copy(const LC& data)
if (p2MCOData != nullptr)
delete[] p2MCOData;
p2MCOData = new uint8_t[P25_P2_IOEMI_MAC_LENGTH_BYTES];
::memset(p2MCOData, 0x00U, P25_P2_IOEMI_MAC_LENGTH_BYTES);
::memcpy(p2MCOData, data.p2MCOData, P25_P2_IOEMI_MAC_LENGTH_BYTES);
m_p2MCODataLength = data.m_p2MCODataLength;
p2MCOData = new uint8_t[m_p2MCODataLength];
::memcpy(p2MCOData, data.p2MCOData, m_p2MCODataLength);
} else {
if (p2MCOData != nullptr) {
delete[] p2MCOData;
p2MCOData = nullptr;
}
m_p2MCODataLength = 0U;
}
s_siteData = data.s_siteData;
@ -1725,46 +1780,26 @@ void LC::encodeHDUGolay(uint8_t* data, const uint8_t* raw)
void LC::decodeP2_DUIDHamming(const uint8_t* data, uint8_t* raw)
{
uint32_t n = 0U;
uint32_t m = 0U;
for (uint32_t i = 0U; i < 4U; i++) {
bool hamming[8U];
for (uint32_t j = 0U; j < 8U; j++) {
hamming[j] = READ_BIT(data, n);
n++;
}
edac::Hamming::decode844(hamming);
bool hamming[8U];
for (uint32_t i = 0U; i < 8U; i++)
hamming[i] = READ_BIT(data, i);
for (uint32_t j = 0U; j < 4U; j++) {
WRITE_BIT(raw, m, hamming[j]);
m++;
}
}
edac::Hamming::decode844(hamming);
for (uint32_t i = 0U; i < 4U; i++)
WRITE_BIT(raw, i, hamming[i]);
}
/* Encode Phase 2 DUID hamming FEC. */
void LC::encodeP2_DUIDHamming(uint8_t* data, const uint8_t* raw)
{
uint32_t n = 0U;
uint32_t m = 0U;
for (uint32_t i = 0U; i < 4U; i++) {
bool hamming[8U];
for (uint32_t j = 0U; j < 4U; j++) {
hamming[j] = READ_BIT(raw, m);
m++;
}
edac::Hamming::encode844(hamming);
bool hamming[8U] = { false };
for (uint32_t i = 0U; i < 4U; i++)
hamming[i] = READ_BIT(raw, i);
for (uint32_t j = 0U; j < 8U; j++) {
WRITE_BIT(data, n, hamming[j]);
n++;
}
}
edac::Hamming::encode844(hamming);
for (uint32_t i = 0U; i < 8U; i++)
WRITE_BIT(data, i, hamming[i]);
}
/* Apply the Phase 2 TDMA MAC burst scrambler. */
@ -1783,9 +1818,9 @@ void LC::applyP2Scrambler(uint8_t* data, bool inbound, bool sync)
uint16_t interFieldSkips[3U] = { 0U, 0U, 0U };
if (inbound) {
cursor = 14U;
cursor = sync ? 14U : 0U;
fieldCount = 4U;
fieldLengths[0U] = 36U;
fieldLengths[0U] = sync ? 36U : 72U;
fieldLengths[1U] = 72U;
fieldLengths[2U] = 96U;
fieldLengths[3U] = 72U;

@ -111,7 +111,7 @@ namespace p25
*/
void encodeLDU2(uint8_t* data);
/** Project 25 Phase II (TIA-102.BBAD-D Section 2) */
/** Project 25 Phase II (TIA-102.BBAD-A Section 2) */
/**
* @brief Decode a IEMI VCH MAC PDU.
@ -120,6 +120,8 @@ namespace p25
* @return bool True, if MAC PDU decoded, otherwise false.
*/
bool decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync);
/** @brief Encode an inbound/IEMI Phase 2 VCH MAC PDU. */
void encodeVCH_MACPDU_IEMI(uint8_t* data, bool sync);
/**
* @brief Decode a xOEMI VCH MAC PDU.
* @param data Buffer containing the MAC PDU to decode.
@ -350,7 +352,9 @@ namespace p25
/** @} */
/** @name Phase 2 Raw MCO Data */
uint8_t* p2MCOData; // ?? - this should probably be private with getters/setters
uint8_t* p2MCOData;
uint32_t getP2MCOData(uint8_t* data, uint32_t length) const;
uint32_t getP2MCODataLength() const { return m_p2MCODataLength; }
/** @} */
private:
@ -373,6 +377,7 @@ namespace p25
bool m_gotUserAliasPartA;
bool m_gotUserAlias;
uint32_t m_p2MCODataLength;
static bool s_warnCRC;

@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/MACPDU.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the MACPDU class. */
MACPDU::MACPDU(uint8_t opcode) :
m_opcode(opcode),
m_srcId(0U),
m_dstId(0U),
m_emergency(false),
m_encrypted(false),
m_priority(0U),
m_group(false)
{
/* stub */
}
/* Decodes a MAC PDU from the specified LC control data. */
bool MACPDU::decode(const LC& control)
{
if (control.getLCO() != m_opcode)
return false;
m_srcId = control.getSrcId();
m_dstId = control.getDstId();
m_group = control.getGroup();
m_emergency = control.getEmergency();
m_encrypted = control.getEncrypted();
m_priority = control.getPriority();
return true;
}
/* Encodes the MAC PDU into the specified LC control data. */
void MACPDU::encode(LC& control) const
{
control.setLCO(m_opcode);
control.setMACPartition(p25::defines::P2_MAC_MCO_PARTITION::UNIQUE);
control.setSrcId(m_srcId);
control.setDstId(m_dstId);
control.setGroup(m_group);
control.setEmergency(m_emergency);
control.setEncrypted(m_encrypted);
control.setPriority(m_priority);
}
/* Returns a string that represents the current MAC PDU. */
std::string MACPDU::toString(bool isp)
{
return std::string("MAC PDU, UNKNOWN (Unknown MAC PDU)");
}

@ -0,0 +1,105 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file MACPDU.h
* @ingroup p25_lc
* @file MACPDU.cpp
* @ingroup p25_lc
*/
#if !defined(__P25_LC__MAC_PDU_H__)
#define __P25_LC__MAC_PDU_H__
#include "common/Defines.h"
#include "common/p25/lc/LC.h"
#include <cstdint>
namespace p25
{
namespace lc
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Base MAC object for a decoded Phase 2 MAC PDU.
* @ingroup p25_lc
*/
class HOST_SW_API MACPDU {
public:
/**
* @brief Initializes a new instance of the MACPDU class.
* @param opcode The message opcode for the MAC PDU.
*/
explicit MACPDU(uint8_t opcode);
/**
* @brief Finalizes a instance of the MACPDU class.
*/
virtual ~MACPDU() = default;
/**
* @brief Decodes a MAC PDU from the specified LC control data.
* @param control The LC control data to decode from.
* @returns bool True, if the MAC PDU was decoded, otherwise false.
*/
virtual bool decode(const LC& control);
/**
* @brief Encodes the MAC PDU into the specified LC control data.
* @param control The LC control data to encode into.
*/
virtual void encode(LC& control) const;
/**
* @brief Returns a string that represents the current MAC PDU.
* @returns std::string String representation of the MAC PDU.
*/
virtual std::string toString(bool isp = false);
public:
/** @name Common Data */
/**
* @brief Message opcode for the MAC PDU.
*/
DECLARE_PROPERTY(uint8_t, opcode, Opcode);
/**
* @brief Source ID.
*/
DECLARE_PROPERTY(uint32_t, srcId, SrcId);
/**
* @brief Destination ID.
*/
DECLARE_PROPERTY(uint32_t, dstId, DstId);
/** @} */
/** @name Service Options */
/**
* @brief Flag indicating the emergency bits are set.
*/
DECLARE_PROPERTY(bool, emergency, Emergency);
/**
* @brief Flag indicating that encryption is enabled.
*/
DECLARE_PROPERTY(bool, encrypted, Encrypted);
/**
* @brief Priority level for the traffic.
*/
DECLARE_PROPERTY(uint8_t, priority, Priority);
/**
* @brief Flag indicating a group/talkgroup operation.
*/
DECLARE_PROPERTY(bool, group, Group);
/** @} */
};
}
}
#endif // __P25_LC__MAC_PDU_H__

@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/mac/MACFactory.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
using namespace p25::lc::mac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Create and decode a MAC PDU from decoded LC data. */
std::unique_ptr<MACPDU> MACFactory::createMACPDU(const LC& control)
{
switch (control.getLCO()) {
case p25::defines::P2_MAC_MCO::GROUP:
return decode(new MAC_GROUP_VCH_USER(), control);
case p25::defines::P2_MAC_MCO::PRIVATE:
return decode(new MAC_UU_VCH_USER(), control);
case p25::defines::P2_MAC_MCO::TEL_INT_VCH_USER:
return decode(new MAC_TEL_INT_VCH_USER(), control);
case p25::defines::P2_MAC_MCO::MAC_RELEASE:
return decode(new MAC_RELEASE(), control);
default:
break;
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Decode a MAC PDU from decoded LC data. */
std::unique_ptr<MACPDU> MACFactory::decode(MACPDU* mac, const LC& control)
{
if (mac == nullptr || !mac->decode(control)) {
delete mac;
return nullptr;
}
return std::unique_ptr<MACPDU>(mac);
}

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @defgroup p25_mac Phase 2 MAC
* @brief Implementation for the data handling of the TIA-102.BBAD Project 25 standard (Phase 2 MAC)
* @ingroup p25_lc
*
* @file MACFactory.h
* @ingroup p25_mac
* @file MACFactory.cpp
* @ingroup p25_mac
*/
#if !defined(__P25_LC_MAC__MAC_FACTORY_H__)
#define __P25_LC_MAC__MAC_FACTORY_H__
#include "common/Defines.h"
#include "common/p25/lc/MACPDU.h"
#include "common/p25/lc/mac/MAC_GROUP_VCH_USER.h"
#include "common/p25/lc/mac/MAC_RELEASE.h"
#include "common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h"
#include "common/p25/lc/mac/MAC_UU_VCH_USER.h"
namespace p25
{
namespace lc
{
namespace mac
{
/**
* @brief Helper class to instantiate a Phase 2 MAC PDU.
* @ingroup p25_lc
*/
class HOST_SW_API MACFactory {
public:
/**
* @brief Initializes a new instance of the MACFactory class.
*/
MACFactory() = default;
/**
* @brief Finalizes a instance of the MACFactory class.
*/
~MACFactory() = default;
/**
* @brief Create and decode a MAC PDU from decoded LC data.
* @param control Decoded Phase 2 MAC PDU fields.
* @returns MACPDU* Instance representing the decoded opcode.
*/
static std::unique_ptr<MACPDU> createMACPDU(const LC& control);
private:
/**
* @brief Decode a MAC PDU from decoded LC data.
* @param mac Instance of MAC PDU to decode into.
* @param control Decoded Phase 2 MAC PDU fields.
* @returns MACPDU* Instance representing the decoded opcode.
*/
static std::unique_ptr<MACPDU> decode(MACPDU* mac, const LC& control);
};
}
}
}
#endif // __P25_LC_MAC__MAC_FACTORY_H__

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/mac/MAC_GROUP_VCH_USER.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
using namespace p25::lc::mac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the MAC_GROUP_VCH_USER class. */
MAC_GROUP_VCH_USER::MAC_GROUP_VCH_USER() : MACPDU(p25::defines::P2_MAC_MCO::GROUP)
{
/* stub */
}
/* Decodes a MAC PDU from the specified LC control data. */
bool MAC_GROUP_VCH_USER::decode(const LC& control)
{
return MACPDU::decode(control) && control.getGroup();
}
/* Returns a string that represents the current MAC PDU. */
std::string MAC_GROUP_VCH_USER::toString(bool isp)
{
return std::string("MAC PDU, GROUP VCH USER (Group Voice Channel User)");
}

@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file MAC_GROUP_VCH_USER.h
* @ingroup p25_mac
* @file MAC_GROUP_VCH_USER.cpp
* @ingroup p25_mac
*/
#if !defined(__P25_LC_MAC__MAC_GROUP_VCH_USER_H__)
#define __P25_LC_MAC__MAC_GROUP_VCH_USER_H__
#include "common/p25/lc/MACPDU.h"
namespace p25
{
namespace lc
{
namespace mac
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Phase 2 group voice channel user MAC PDU.
*/
class HOST_SW_API MAC_GROUP_VCH_USER : public MACPDU {
public:
/**
* @brief Initializes a new instance of the MAC_GROUP_VCH_USER class.
*/
MAC_GROUP_VCH_USER();
/**
* @brief Decodes a MAC PDU from the specified LC control data.
* @param control The LC control data to decode from.
* @returns bool True, if the MAC PDU was decoded, otherwise false.
*/
bool decode(const LC& control) override;
/**
* @brief Returns a string that represents the current MAC PDU.
* @returns std::string String representation of the MAC PDU.
*/
std::string toString(bool isp = false);
};
}
}
}
#endif // __P25_LC_MAC__MAC_GROUP_VCH_USER_H__

@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/mac/MAC_RELEASE.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
using namespace p25::lc::mac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the MAC_RELEASE class. */
MAC_RELEASE::MAC_RELEASE() : MACPDU(p25::defines::P2_MAC_MCO::MAC_RELEASE)
{
/* stub */
}
/* Returns a string that represents the current MAC PDU. */
std::string MAC_RELEASE::toString(bool isp)
{
return std::string("MAC PDU, RELEASE (Release)");
}

@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file MAC_RELEASE.h
* @ingroup p25_mac
* @file MAC_RELEASE.cpp
* @ingroup p25_mac
*/
#if !defined(__P25_LC_MAC__MAC_RELEASE_H__)
#define __P25_LC_MAC__MAC_RELEASE_H__
#include "common/p25/lc/MACPDU.h"
namespace p25
{
namespace lc
{
namespace mac
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Phase 2 MAC release PDU.
*/
class HOST_SW_API MAC_RELEASE : public MACPDU {
public:
/**
* @brief Initializes a new instance of the MAC_RELEASE class.
*/
MAC_RELEASE();
/**
* @brief Returns a string that represents the current MAC PDU.
* @returns std::string String representation of the MAC PDU.
*/
std::string toString(bool isp = false);
};
}
}
}
#endif // __P25_LC_MAC__MAC_RELEASE_H__

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
using namespace p25::lc::mac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the MAC_TEL_INT_VCH_USER class. */
MAC_TEL_INT_VCH_USER::MAC_TEL_INT_VCH_USER() : MACPDU(p25::defines::P2_MAC_MCO::TEL_INT_VCH_USER)
{
/* stub */
}
/* Decodes a MAC PDU from the specified LC control data. */
bool MAC_TEL_INT_VCH_USER::decode(const LC& control)
{
return MACPDU::decode(control);
}
/* Returns a string that represents the current MAC PDU. */
std::string MAC_TEL_INT_VCH_USER::toString(bool isp)
{
return std::string("MAC PDU, TEL INT VCH USER (Telephone Interconnect Voice Channel User)");
}

@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file MAC_TEL_INT_VCH_USER.h
* @ingroup p25_mac
* @file MAC_TEL_INT_VCH_USER.cpp
* @ingroup p25_mac
*/
#if !defined(__P25_LC_MAC__MAC_TEL_INT_VCH_USER_H__)
#define __P25_LC_MAC__MAC_TEL_INT_VCH_USER_H__
#include "common/p25/lc/MACPDU.h"
namespace p25
{
namespace lc
{
namespace mac
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Phase 2 telephone interconnect voice channel user MAC PDU.
*/
class HOST_SW_API MAC_TEL_INT_VCH_USER : public MACPDU {
public:
/**
* @brief Initializes a new instance of the MAC_TEL_INT_VCH_USER class.
*/
MAC_TEL_INT_VCH_USER();
/**
* @brief Decodes a MAC PDU from the specified LC control data.
* @param control The LC control data to decode from.
* @returns bool True, if the MAC PDU was decoded, otherwise false.
*/
bool decode(const LC& control) override;
/**
* @brief Returns a string that represents the current MAC PDU.
* @returns std::string String representation of the MAC PDU.
*/
std::string toString(bool isp = false);
};
}
}
}
#endif // __P25_LC_MAC__MAC_TEL_INT_VCH_USER_H__

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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 "common/p25/lc/mac/MAC_UU_VCH_USER.h"
#include "common/p25/P25Defines.h"
using namespace p25::lc;
using namespace p25::lc::mac;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the MAC_UU_VCH_USER class. */
MAC_UU_VCH_USER::MAC_UU_VCH_USER() : MACPDU(p25::defines::P2_MAC_MCO::PRIVATE)
{
/* stub */
}
/* Decodes a MAC PDU from the specified LC control data. */
bool MAC_UU_VCH_USER::decode(const LC& control)
{
return MACPDU::decode(control) && !control.getGroup();
}
/* Returns a string that represents the current MAC PDU. */
std::string MAC_UU_VCH_USER::toString(bool isp)
{
return std::string("MAC PDU, UU VCH USER (Unit-to-Unit Voice Channel User)");
}

@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* 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
*
*/
/**
* @file MAC_UU_VCH_USER.h
* @ingroup p25_mac
* @file MAC_UU_VCH_USER.cpp
* @ingroup p25_mac
*/
#if !defined(__P25_LC_MAC__MAC_UU_VCH_USER_H__)
#define __P25_LC_MAC__MAC_UU_VCH_USER_H__
#include "common/p25/lc/MACPDU.h"
namespace p25
{
namespace lc
{
namespace mac
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Phase 2 unit-to-unit voice channel user MAC PDU.
*/
class HOST_SW_API MAC_UU_VCH_USER : public MACPDU {
public:
/**
* @brief Initializes a new instance of the MAC_UU_VCH_USER class.
*/
MAC_UU_VCH_USER();
/**
* @brief Decodes a MAC PDU from the specified LC control data.
* @param control The LC control data to decode from.
* @returns bool True, if the MAC PDU was decoded, otherwise false.
*/
bool decode(const LC& control) override;
/**
* @brief Returns a string that represents the current MAC PDU.
* @returns std::string String representation of the MAC PDU.
*/
std::string toString(bool isp = false);
};
}
}
}
#endif // __P25_LC_MAC__MAC_UU_VCH_USER_H__

@ -22,12 +22,16 @@ file(GLOB dvmhost_SRC
# P25 module
"src/host/p25/*.h"
"src/host/p25/*.cpp"
"src/host/p25/phase2/*.h"
"src/host/p25/phase2/*.cpp"
"src/host/p25/lc/tsbk/*.h"
"src/host/p25/lc/tsbk/*.cpp"
"src/host/p25/lookups/*.h"
"src/host/p25/lookups/*.cpp"
"src/host/p25/packet/*.h"
"src/host/p25/packet/*.cpp"
"src/host/p25/packet/phase2/*.h"
"src/host/p25/packet/phase2/*.cpp"
# NXDN module
"src/host/nxdn/*.h"

@ -129,6 +129,7 @@ Modem::Modem(port::IModemPort* port, bool duplex, bool rxInvert, bool txInvert,
m_adcOverFlowCount(0U),
m_dacOverFlowCount(0U),
m_v24Connected(false),
m_p25P2Capable(false),
m_modemState(STATE_IDLE),
m_buffer(nullptr),
m_length(0U),
@ -142,12 +143,16 @@ Modem::Modem(port::IModemPort* port, bool duplex, bool rxInvert, bool txInvert,
m_rxDMRQueue1(dmrQueueSize, "Modem RX DMR1"),
m_rxDMRQueue2(dmrQueueSize, "Modem RX DMR2"),
m_rxP25Queue(p25QueueSize, "Modem RX P25"),
m_rxP25P2Queue1(p25QueueSize, "Modem RX P25 P2 Slot 1"),
m_rxP25P2Queue2(p25QueueSize, "Modem RX P25 P2 Slot 2"),
m_rxNXDNQueue(nxdnQueueSize, "Modem RX NXDN"),
m_statusTimer(1000U, 0U, MODEM_POLL_TIME),
m_inactivityTimer(1000U, 8U),
m_dmrSpace1(0U),
m_dmrSpace2(0U),
m_p25Space(0U),
m_p25P2Space1(0U),
m_p25P2Space2(0U),
m_nxdnSpace(0U),
m_tx(false),
m_cd(false),
@ -697,6 +702,117 @@ void Modem::clock(uint32_t ms)
}
break;
/** Project 25 Phase 2 */
case CMD_P25_P2_DATA1:
{
if (m_p25Enabled) {
if (!m_p25P2Capable) {
break;
}
std::lock_guard<std::mutex> lock(m_p25P2ReadLock1);
if (m_rspDoubleLength) {
LogError(LOG_MODEM, "CMD_P25_P2_DATA1 double length?; len = %u", m_length);
break;
}
// Firmware supplies DUID transport metadata followed by the
// 40-byte logical burst. The DUID is not part of the air burst.
if (m_length != (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U)) {
LogError(LOG_MODEM, "CMD_P25_P2_DATA1 invalid length; len = %u", m_length);
break;
}
uint8_t data = m_length - 2U;
m_rxP25P2Queue1.addData(&data, 1U);
data = TAG_DATA;
m_rxP25P2Queue1.addData(&data, 1U);
m_rxP25P2Queue1.addData(m_buffer + 3U, m_length - 3U);
if (m_trace)
Utils::dump(1U, "Modem::clock(), RX P25 P2 Data 1", m_buffer + 3U, m_length - 3U);
}
}
break;
case CMD_P25_P2_DATA2:
{
if (m_p25Enabled) {
if (!m_p25P2Capable) {
break;
}
std::lock_guard<std::mutex> lock(m_p25P2ReadLock2);
if (m_rspDoubleLength) {
LogError(LOG_MODEM, "CMD_P25_P2_DATA2 double length?; len = %u", m_length);
break;
}
if (m_length != (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U)) {
LogError(LOG_MODEM, "CMD_P25_P2_DATA2 invalid length; len = %u", m_length);
break;
}
uint8_t data = m_length - 2U;
m_rxP25P2Queue2.addData(&data, 1U);
data = TAG_DATA;
m_rxP25P2Queue2.addData(&data, 1U);
m_rxP25P2Queue2.addData(m_buffer + 3U, m_length - 3U);
if (m_trace)
Utils::dump(1U, "Modem::clock(), RX P25 P2 Data 2", m_buffer + 3U, m_length - 3U);
}
}
break;
case CMD_P25_P2_LOST1:
{
if (m_p25Enabled) {
if (!m_p25P2Capable) {
break;
}
std::lock_guard<std::mutex> lock(m_p25P2ReadLock1);
if (m_rspDoubleLength) {
LogError(LOG_MODEM, "CMD_P25_P2_LOST1 double length?; len = %u", m_length);
break;
}
uint8_t data = 1U;
m_rxP25P2Queue1.addData(&data, 1U);
data = TAG_LOST;
m_rxP25P2Queue1.addData(&data, 1U);
}
}
break;
case CMD_P25_P2_LOST2:
{
if (m_p25Enabled) {
if (!m_p25P2Capable) {
break;
}
std::lock_guard<std::mutex> lock(m_p25P2ReadLock2);
if (m_rspDoubleLength) {
LogError(LOG_MODEM, "CMD_P25_P2_LOST2 double length?; len = %u", m_length);
break;
}
uint8_t data = 1U;
m_rxP25P2Queue2.addData(&data, 1U);
data = TAG_LOST;
m_rxP25P2Queue2.addData(&data, 1U);
}
}
break;
/** Next Generation Digital Narrowband */
case CMD_NXDN_DATA:
{
@ -872,6 +988,13 @@ void Modem::clock(uint32_t ms)
LogWarning(LOG_MODEM, "NAK, %s, p25Space = %u", rsnToString(m_buffer[4U]).c_str(), m_p25Space);
break;
case CMD_P25_P2_DATA1:
LogWarning(LOG_MODEM, "NAK, %s, p25P2Space1 = %u", rsnToString(m_buffer[4U]).c_str(), m_p25P2Space1);
break;
case CMD_P25_P2_DATA2:
LogWarning(LOG_MODEM, "NAK, %s, p25P2Space2 = %u", rsnToString(m_buffer[4U]).c_str(), m_p25P2Space2);
break;
case CMD_NXDN_DATA:
LogWarning(LOG_MODEM, "NAK, %s, nxdnSpace = %u", rsnToString(m_buffer[4U]).c_str(), m_nxdnSpace);
break;
@ -1092,6 +1215,130 @@ uint32_t Modem::readP25Frame(uint8_t* data)
return 0U;
}
/* Get the frame data length for the next P25 Phase 2 Slot 1 frame. */
uint32_t Modem::peekP25P2Frame1Length()
{
if (!m_p25P2Capable)
return 0U;
std::lock_guard<std::mutex> lock(m_p25P2ReadLock1);
if (m_rxP25P2Queue1.isEmpty())
return 0U;
uint8_t len = 0U;
m_rxP25P2Queue1.peek(&len, 1U);
#if DEBUG_MODEM
LogDebugEx(LOG_MODEM, "Modem::peekP25P2Frame1Length()", "len = %u, dataSize = %u", len, m_rxP25P2Queue2.dataSize());
#endif
// this ensures we never get in a situation where we have length stuck on the queue
if (m_rxP25P2Queue1.dataSize() == 1U && len > m_rxP25P2Queue1.dataSize()) {
m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off
return 0U;
}
if (m_rxP25P2Queue1.dataSize() > len) {
return len;
}
return 0U;
}
/* Reads P25 Phase 2 Slot 1 frame data. */
uint32_t Modem::readP25P2Frame1(uint8_t* data)
{
assert(data != nullptr);
if (!m_p25P2Capable)
return 0U;
std::lock_guard<std::mutex> lock(m_p25P2ReadLock1);
if (m_rxP25P2Queue1.isEmpty())
return 0U;
uint8_t len = 0U;
m_rxP25P2Queue1.peek(&len, 1U);
// this ensures we never get in a situation where we have length stuck on the queue
if (m_rxP25P2Queue1.dataSize() == 1U && len > m_rxP25P2Queue1.dataSize()) {
m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off
return 0U;
}
if (m_rxP25P2Queue1.dataSize() > len) {
m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off
m_rxP25P2Queue1.get(data, len);
return len;
}
return 0U;
}
/* Get the frame data length for the next P25 Phase 2 Slot 2 frame. */
uint32_t Modem::peekP25P2Frame2Length()
{
if (!m_p25P2Capable)
return 0U;
std::lock_guard<std::mutex> lock(m_p25P2ReadLock2);
if (m_rxP25P2Queue2.isEmpty())
return 0U;
uint8_t len = 0U;
m_rxP25P2Queue2.peek(&len, 1U);
#if DEBUG_MODEM
LogDebugEx(LOG_MODEM, "Modem::peekP25P2Frame2Length()", "len = %u, dataSize = %u", len, m_rxP25P2Queue2.dataSize());
#endif
// this ensures we never get in a situation where we have length stuck on the queue
if (m_rxP25P2Queue2.dataSize() == 1U && len > m_rxP25P2Queue2.dataSize()) {
m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off
return 0U;
}
if (m_rxP25P2Queue2.dataSize() > len) {
return len;
}
return 0U;
}
/* Reads P25 Phase 2 Slot 2 frame data. */
uint32_t Modem::readP25P2Frame2(uint8_t* data)
{
assert(data != nullptr);
if (!m_p25P2Capable)
return 0U;
std::lock_guard<std::mutex> lock(m_p25P2ReadLock2);
if (m_rxP25P2Queue2.isEmpty())
return 0U;
uint8_t len = 0U;
m_rxP25P2Queue2.peek(&len, 1U);
// this ensures we never get in a situation where we have length stuck on the queue
if (m_rxP25P2Queue2.dataSize() == 1U && len > m_rxP25P2Queue2.dataSize()) {
m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off
return 0U;
}
if (m_rxP25P2Queue2.dataSize() > len) {
m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off
m_rxP25P2Queue2.get(data, len);
return len;
}
return 0U;
}
/* Get the frame data length for the next frame in the NXDN ring buffer. */
uint32_t Modem::peekNXDNFrameLength()
@ -1167,6 +1414,20 @@ bool Modem::hasP25Space(uint32_t length) const
return m_p25Space >= length;
}
/* Helper to test if the P25 Phase 2 Slot 1 ring buffer has free space. */
bool Modem::hasP25P2Space1() const
{
return m_p25P2Space1 >= (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 2U);
}
/* Helper to test if the P25 Phase 2 Slot 2 ring buffer has free space. */
bool Modem::hasP25P2Space2() const
{
return m_p25P2Space2 >= (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 2U);
}
/* Helper to test if the NXDN ring buffer has free space. */
bool Modem::hasNXDNSpace() const
@ -1264,6 +1525,44 @@ void Modem::clearP25Frame()
Thread::sleep(5); // 5ms delay
}
/* Clears any buffered P25 Phase 2 Slot 1 frame data to be sent to the air interface modem. */
void Modem::clearP25P2Frame1()
{
if (!m_p25P2Capable)
return;
uint8_t buffer[3U];
buffer[0U] = DVM_SHORT_FRAME_START;
buffer[1U] = 3U;
buffer[2U] = CMD_P25_P2_CLEAR1;
#if DEBUG_MODEM
Utils::dump(1U, "Modem::clearP25P2Frame1(), Written", buffer, 3U);
#endif
write(buffer, 3U);
Thread::sleep(5); // 5ms delay
}
/* Clears any buffered P25 Phase 2 Slot 2 frame data to be sent to the air interface modem. */
void Modem::clearP25P2Frame2()
{
if (!m_p25P2Capable)
return;
uint8_t buffer[3U];
buffer[0U] = DVM_SHORT_FRAME_START;
buffer[1U] = 3U;
buffer[2U] = CMD_P25_P2_CLEAR2;
#if DEBUG_MODEM
Utils::dump(1U, "Modem::clearP25P2Frame2(), Written", buffer, 3U);
#endif
write(buffer, 3U);
Thread::sleep(5); // 5ms delay
}
/* Clears any buffered NXDN frame data to be sent to the air interface modem. */
void Modem::clearNXDNFrame()
@ -1349,6 +1648,62 @@ void Modem::injectP25Frame(const uint8_t* data, uint32_t length)
}
}
/* Internal helper to inject P25 Phase 2 Slot 1 frame data. */
void Modem::injectP25P2Frame1(const uint8_t* data, uint32_t length)
{
assert(data != nullptr);
assert(length > 0U);
if (m_p25Enabled) {
if (!m_p25P2Capable)
return;
if (m_trace)
Utils::dump(1U, "Injected P25 Phase 2 Slot 1 Data", data, length);
uint8_t val = length + 2U;
m_rxP25P2Queue1.addData(&val, 1U);
val = TAG_DATA;
m_rxP25P2Queue1.addData(&val, 1U);
// Injection defaults to a 4V logical voice burst. Tests needing MAC
// signaling should inject the complete typed modem envelope instead.
val = P25DEF::P2_DUID::VTCH_4V;
m_rxP25P2Queue1.addData(&val, 1U);
m_rxP25P2Queue1.addData(data, length);
}
}
/* Internal helper to inject P25 Phase 2 Slot 2 frame data. */
void Modem::injectP25P2Frame2(const uint8_t* data, uint32_t length)
{
assert(data != nullptr);
assert(length > 0U);
if (m_p25Enabled) {
if (!m_p25P2Capable)
return;
if (m_trace)
Utils::dump(1U, "Injected P25 Phase 2 Slot 2 Data", data, length);
uint8_t val = length + 2U;
m_rxP25P2Queue2.addData(&val, 1U);
val = TAG_DATA;
m_rxP25P2Queue2.addData(&val, 1U);
val = P25DEF::P2_DUID::VTCH_4V;
m_rxP25P2Queue2.addData(&val, 1U);
m_rxP25P2Queue2.addData(data, length);
}
}
/* Internal helper to inject NXDN frame data as if it came from the air interface modem. */
void Modem::injectNXDNFrame(const uint8_t* data, uint32_t length)
@ -1562,6 +1917,137 @@ bool Modem::writeP25Frame(const uint8_t* data, uint32_t length, bool imm)
}
}
/* Writes P25 Phase 2 Slot 1 frame data to the P25 Phase 2 Slot 1 ring buffer. */
bool Modem::writeP25P2Frame1(const uint8_t* data, uint32_t length, bool imm)
{
assert(data != nullptr);
assert(length > 0U);
if (m_p25Enabled) {
if (!m_p25P2Capable)
return false;
const uint8_t MAX_LENGTH = 48U;
if (data[0U] != TAG_DATA && data[0U] != TAG_EOT)
return false;
if (length > MAX_LENGTH) {
LogError(LOG_MODEM, "Modem::writeP25P2Frame1(); request data to write >%u?, len = %u", MAX_LENGTH, length);
Utils::dump(1U, "Modem::writeP25P2Frame1(), Attempted Data", data, length);
return false;
}
uint8_t buffer[MAX_LENGTH];
buffer[0U] = DVM_SHORT_FRAME_START;
buffer[1U] = length + 2U;
buffer[2U] = CMD_P25_P2_DATA1;
// The serial payload is DUID + 40-byte logical burst. Firmware uses the
// DUID to select a legal voice/FACCH/SACCH opportunity; it must not air
// the metadata octet.
// TODO(P25P2-FW): Implement the typed per-LCH scheduler, ISCH insertion,
// superframe/ultraframe tracking, and scrambler phase in modem firmware.
::memcpy(buffer + 3U, data + 1U, length - 1U);
uint8_t len = length + 2U;
// write or buffer P25 Phase 2 slot 1 data to air interface
if (m_p25P2Space1 >= length) {
if (m_debug)
LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame1()", "immediate write (len %u)", length);
if (m_trace)
Utils::dump(1U, "Modem::writeP25P2Frame1(), Immediate TX P25 Phase 2 Slot 1 Data", buffer + 3U, length - 1U);
int ret = write(buffer, len, imm);
if (ret != int(len)) {
LogError(LOG_MODEM, "Error writing P25 Phase 2 Slot 1 data");
return false;
}
m_p25P2Space1 -= length;
if ((int32_t)m_p25P2Space1 < 0) {
if (m_debug)
LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame1()", "m_p25P2Space1 underflow, space = %u, length = %u", m_p25P2Space1, length);
m_p25P2Space1 = 0U;
}
}
else {
return false;
}
return true;
}
else {
return false;
}
}
/* Writes P25 Phase 2 Slot 2 frame data to the P25 Phase 2 Slot 2 ring buffer. */
bool Modem::writeP25P2Frame2(const uint8_t* data, uint32_t length, bool imm)
{
assert(data != nullptr);
assert(length > 0U);
if (m_p25Enabled) {
if (!m_p25P2Capable)
return false;
const uint8_t MAX_LENGTH = 48U;
if (data[0U] != TAG_DATA && data[0U] != TAG_EOT)
return false;
if (length > MAX_LENGTH) {
LogError(LOG_MODEM, "Modem::writeP25P2Frame2(); request data to write >%u?, len = %u", MAX_LENGTH, length);
Utils::dump(1U, "Modem::writeP25P2Frame2(), Attempted Data", data, length);
return false;
}
uint8_t buffer[MAX_LENGTH];
buffer[0U] = DVM_SHORT_FRAME_START;
buffer[1U] = length + 2U;
buffer[2U] = CMD_P25_P2_DATA2;
// TODO(P25P2-FW): Treat immediate as "next legal opportunity for this
// DUID", never as permission to violate FACCH/SACCH/LCH placement.
::memcpy(buffer + 3U, data + 1U, length - 1U);
uint8_t len = length + 2U;
// write or buffer P25 Phase 2 slot 2 data to air interface
if (m_p25P2Space2 >= length) {
if (m_debug)
LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame2()", "immediate write (len %u)", length);
if (m_trace)
Utils::dump(1U, "Modem::writeP25P2Frame2(), Immediate TX P25 Phase 2 Slot 2 Data", buffer + 3U, length - 1U);
int ret = write(buffer, len, imm);
if (ret != int(len)) {
LogError(LOG_MODEM, "Error writing P25 Phase 2 Slot 2 data");
return false;
}
m_p25P2Space2 -= length;
if ((int32_t)m_p25P2Space2 < 0) {
if (m_debug)
LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame2()", "m_p25P2Space2 underflow, space = %u, length = %u", m_p25P2Space2, length);
m_p25P2Space2 = 0U;
}
}
else {
return false;
}
return true;
}
else {
return false;
}
}
/* Writes NXDN frame data to the NXDN ring buffer. */
bool Modem::writeNXDNFrame(const uint8_t* data, uint32_t length, bool imm)

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2011-2021 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
* Copyright (C) 2021 Nat Moore
*
*/
@ -28,6 +28,7 @@
#include "Defines.h"
#include "common/RingBuffer.h"
#include "common/p25/P25Defines.h"
#include "common/Timer.h"
#include "modem/port/IModemPort.h"
#include "restapi/RESTAPI.h"
@ -172,6 +173,13 @@ namespace modem
CMD_P25_LOST = 0x32U, //!< Project 25 Data Lost
CMD_P25_CLEAR = 0x33U, //!< Project 25 Clear Buffer
CMD_P25_P2_DATA1 = 0x34U, //!< Project 25 Phase 2 Data Slot 1
CMD_P25_P2_LOST1 = 0x35U, //!< Project 25 Phase 2 Data Lost Slot 1
CMD_P25_P2_DATA2 = 0x36U, //!< Project 25 Phase 2 Data Slot 2
CMD_P25_P2_LOST2 = 0x37U, //!< Project 25 Phase 2 Data Lost Slot 2
CMD_P25_P2_CLEAR1 = 0x38U, //!< Project 25 Phase 2 Clear Slot 1 Buffer
CMD_P25_P2_CLEAR2 = 0x39U, //!< Project 25 Phase 2 Clear Slot 2 Buffer
CMD_NXDN_DATA = 0x41U, //!< NXDN Data
CMD_NXDN_LOST = 0x42U, //!< NXDN Data Lost
CMD_NXDN_CLEAR = 0x43U, //!< NXDN Clear Buffer
@ -469,6 +477,28 @@ namespace modem
* @returns uint32_t Length of data read from ring buffer.
*/
uint32_t readP25Frame(uint8_t* data);
/**
* @brief Get the frame data length for the next P25 Phase 2 Slot 1 frame.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t peekP25P2Frame1Length();
/**
* @brief Reads P25 Phase 2 Slot 1 frame data.
* @param[out] data Buffer to write frame data to.
* @returns uint32_t Length of data read from ring buffer.
*/
uint32_t readP25P2Frame1(uint8_t* data);
/**
* @brief Get the frame data length for the next P25 Phase 2 Slot 2 frame.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t peekP25P2Frame2Length();
/**
* @brief Reads P25 Phase 2 Slot 2 frame data.
* @param[out] data Buffer to write frame data to.
* @returns uint32_t Length of data read from ring buffer.
*/
uint32_t readP25P2Frame2(uint8_t* data);
/**
* @brief Get the frame data length for the next frame in the NXDN ring buffer.
* @returns uint32_t Length of frame data retrieved.
@ -511,6 +541,26 @@ namespace modem
* @return uint32_t Size in bytes available for the P25 ring buffer.
*/
uint32_t getP25Space() const { return m_p25Space; }
/**
* @brief Helper to test if the P25 Phase 2 Slot 1 ring buffer has free space.
* @returns bool True, if the P25 Phase 2 Slot 1 ring buffer has free space, otherwise false.
*/
bool hasP25P2Space1() const;
/**
* @brief Helper to return the currently reported available P25 Phase 2 Slot 1 ring buffer free space.
* @return uint32_t Size in bytes available for the P25 Phase 2 Slot 1 ring buffer.
*/
uint32_t getP25P2Space1() const { return m_p25P2Space1; }
/**
* @brief Helper to test if the P25 Phase 2 Slot 2 ring buffer has free space.
* @returns bool True, if the P25 Phase 2 Slot 2 ring buffer has free space, otherwise false.
*/
bool hasP25P2Space2() const;
/**
* @brief Helper to return the currently reported available P25 Phase 2 Slot 2 ring buffer free space.
* @return uint32_t Size in bytes available for the P25 Phase 2 Slot 2 ring buffer.
*/
uint32_t getP25P2Space2() const { return m_p25P2Space2; }
/**
* @brief Helper to test if the NXDN ring buffer has free space.
* @returns bool True, if the NXDN ring buffer has free space, otherwise false.
@ -568,6 +618,14 @@ namespace modem
* @brief Clears any buffered P25 frame data to be sent to the air interface modem.
*/
void clearP25Frame();
/**
* @brief Clears any buffered P25 Phase 2 Slot 1 frame data to be sent to the air interface modem.
*/
void clearP25P2Frame1();
/**
* @brief Clears any buffered P25 Phase 2 Slot 2 frame data to be sent to the air interface modem.
*/
void clearP25P2Frame2();
/**
* @brief Clears any buffered NXDN frame data to be sent to the air interface modem.
*/
@ -591,6 +649,18 @@ namespace modem
* @param length Length of data to write.
*/
void injectP25Frame(const uint8_t* data, uint32_t length);
/**
* @brief Internal helper to inject P25 Phase 2 Slot 1 frame data.
* @param[in] data Data to write to ring buffer.
* @param length Length of data to write.
*/
void injectP25P2Frame1(const uint8_t* data, uint32_t length);
/**
* @brief Internal helper to inject P25 Phase 2 Slot 2 frame data.
* @param[in] data Data to write to ring buffer.
* @param length Length of data to write.
*/
void injectP25P2Frame2(const uint8_t* data, uint32_t length);
/**
* @brief Internal helper to inject NXDN frame data as if it came from the air interface modem.
* @param[in] data Data to write to ring buffer.
@ -622,6 +692,22 @@ namespace modem
* @returns bool True, if data is written, otherwise false.
*/
bool writeP25Frame(const uint8_t* data, uint32_t length, bool imm = false);
/**
* @brief Write P25 Phase 2 Slot 1 frame data to the P25 Phase 2 Slot 1 ring buffer.
* @param[in] data Data to write to ring buffer.
* @param length Length of data to write.
* @param imm Flag indicating whether the frame is immediate.
* @returns bool True, if data is written, otherwise false.
*/
bool writeP25P2Frame1(const uint8_t* data, uint32_t length, bool imm = false);
/**
* @brief Writes P25 Phase 2 Slot 2 frame data to the P25 Phase 2 Slot 2 ring buffer.
* @param[in] data Data to write to ring buffer.
* @param length Length of data to write.
* @param imm Flag indicating whether the frame is immediate.
* @returns bool True, if data is written, otherwise false.
*/
bool writeP25P2Frame2(const uint8_t* data, uint32_t length, bool imm = false);
/**
* @brief Writes NXDN frame data to the NXDN ring buffer.
* @param[in] data Data to write to ring buffer.
@ -791,6 +877,7 @@ namespace modem
uint32_t m_dacOverFlowCount; // dedicated modem - DAC overflow count
bool m_v24Connected;
bool m_p25P2Capable;
DVM_STATE m_modemState;
uint8_t* m_buffer;
@ -807,6 +894,8 @@ namespace modem
RingBuffer<uint8_t> m_rxDMRQueue1;
RingBuffer<uint8_t> m_rxDMRQueue2;
RingBuffer<uint8_t> m_rxP25Queue;
RingBuffer<uint8_t> m_rxP25P2Queue1;
RingBuffer<uint8_t> m_rxP25P2Queue2;
RingBuffer<uint8_t> m_rxNXDNQueue;
Timer m_statusTimer;
@ -815,6 +904,8 @@ namespace modem
uint32_t m_dmrSpace1;
uint32_t m_dmrSpace2;
uint32_t m_p25Space;
uint32_t m_p25P2Space1;
uint32_t m_p25P2Space2;
uint32_t m_nxdnSpace;
bool m_tx;
@ -825,6 +916,8 @@ namespace modem
std::mutex m_dmr1ReadLock;
std::mutex m_dmr2ReadLock;
std::mutex m_p25ReadLock;
std::mutex m_p25P2ReadLock1;
std::mutex m_p25P2ReadLock2;
std::mutex m_nxdnReadLock;
bool m_ignoreModemConfigArea;

@ -0,0 +1,265 @@
// SPDX-License-Identifier: GPL-2.0-only
/**
* Digital Voice Modem - Modem Host Software
* 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 "Defines.h"
#include "common/p25/P25Defines.h"
#include "common/Log.h"
#include "p25/packet/phase2/Voice.h"
#include "p25/phase2/Control.h"
#include "p25/phase2/Slot.h"
#include <cstring>
using namespace p25::phase2;
using namespace p25::defines;
using namespace p25::packet;
using namespace p25::packet::phase2;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Resets RF voice counters. */
void Voice::resetRF()
{
m_rfFrames = 0U;
m_rfESSBCount = 0U;
::memset(m_rfESS, 0x00U, sizeof(m_rfESS));
::memset(m_rfESSB, 0x00U, sizeof(m_rfESSB));
m_rfESSA = false;
m_rfESSComplete = false;
}
/* Resets network voice counters. */
void Voice::resetNet()
{
m_netFrames = 0U;
m_netESSBCount = 0U;
::memset(m_netESS, 0x00U, sizeof(m_netESS));
::memset(m_netESSB, 0x00U, sizeof(m_netESSB));
m_netESSA = false;
m_netESSComplete = false;
}
/* Process a data frame from the RF interface. */
bool Voice::process(uint8_t* data, uint32_t length)
{
if (!validFrame(data, length) || m_slot == nullptr) {
if (m_debug)
LogDebugEx(LOG_P25, "Voice::process()", "invalid RF voice frame, length = %u", length);
return false;
}
defines::P2_DUID::E duid = m_slot->m_duid;
if (duid != defines::P2_DUID::VTCH_4V && duid != defines::P2_DUID::VTCH_2V) {
if (m_debug)
LogDebugEx(LOG_P25, "Voice::process()", "slot = %u, unexpected voice DUID = $%02X", m_slot->m_slotNo + 1U, (uint8_t)duid);
return false;
}
uint8_t rfFrame[P25_P2_FRAME_LENGTH_BYTES];
::memcpy(rfFrame, data, P25_P2_FRAME_LENGTH_BYTES);
const bool fourVoice = duid == defines::P2_DUID::VTCH_4V;
const uint32_t errors = m_ambe2FEC.regenerateBurst(rfFrame, true, fourVoice);
if (duid == defines::P2_DUID::VTCH_4V) {
processESS4V(rfFrame, m_rfESSBCount % 4U, false);
++m_rfESSBCount;
}
else
processESS2V(rfFrame, false);
if (!m_slot->m_rfTimeout && !m_slot->addFrame(rfFrame, length, duid))
return false;
if (!m_slot->m_rfTimeout && m_slot->s_control != nullptr)
m_slot->s_control->writeNetwork(m_slot, rfFrame, length);
if (m_debug)
LogDebugEx(LOG_P25, "Voice::process()", "slot = %u, RF frame = %u, duid = $%02X",
m_slot->m_slotNo, m_rfFrames, (uint8_t)m_slot->m_duid);
if (m_verbose)
LogInfoEx(LOG_RF, "P25 Phase 2 Slot %u, RF voice frame = %u, srcId = %u, dstId = %u, errs = %u/%u (%.1f%%)",
m_slot->m_slotNo + 1U, m_rfFrames, m_slot->m_control.getSrcId(),
m_slot->m_control.getDstId(), errors, fourVoice ? 288U : 144U,
float(errors * 100U) / float(fourVoice ? 288U : 144U));
m_rfFrames++;
++m_slot->m_rfFrames;
m_slot->m_rfBits += fourVoice ? 288U : 144U;
m_slot->m_rfErrs += errors;
return true;
}
/* Process a data frame from the network. */
bool Voice::processNetwork(uint8_t* data, uint32_t length)
{
if (!validFrame(data, length) || m_slot == nullptr) {
if (m_debug)
LogDebugEx(LOG_P25, "Voice::processNetwork()", "invalid network voice frame, length = %u", length);
return false;
}
defines::P2_DUID::E duid = m_slot->m_duid;
if (duid != defines::P2_DUID::VTCH_4V && duid != defines::P2_DUID::VTCH_2V) {
if (m_debug)
LogDebugEx(LOG_P25, "Voice::processNetwork()", "slot = %u, unexpected voice DUID = $%02X", m_slot->m_slotNo + 1U, (uint8_t)duid);
return false;
}
uint8_t rfFrame[P25_P2_FRAME_LENGTH_BYTES];
::memcpy(rfFrame, data, P25_P2_FRAME_LENGTH_BYTES);
const bool fourVoice = duid == defines::P2_DUID::VTCH_4V;
const uint32_t errors = m_ambe2FEC.regenerateBurst(rfFrame, false, fourVoice);
if (duid == defines::P2_DUID::VTCH_4V) {
processESS4V(rfFrame, m_netESSBCount % 4U, true);
++m_netESSBCount;
}
else
processESS2V(rfFrame, true);
if (m_verbose)
LogInfoEx(LOG_NET, "P25 Phase 2 Slot %u, network voice frame = %u, srcId = %u, dstId = %u, errs = %u/%u (%.1f%%)",
m_slot->m_slotNo + 1U, m_netFrames, m_slot->m_control.getSrcId(),
m_slot->m_control.getDstId(), errors, fourVoice ? 288U : 144U,
float(errors * 100U) / float(fourVoice ? 288U : 144U));
if (!m_slot->m_netTimeout && !m_slot->addFrame(rfFrame, length, duid, true))
return false;
m_netFrames++;
++m_slot->m_netFrames;
m_slot->m_netBits += fourVoice ? 288U : 144U;
m_slot->m_netErrs += errors;
return true;
}
/* Builds and queues an outbound SACCH voice-channel-user PDU. */
bool Voice::writeVoiceLC(uint8_t opcode, bool imm)
{
if (m_slot == nullptr ||
(opcode != P2_MAC_HEADER_OPCODE::ACTIVE && opcode != P2_MAC_HEADER_OPCODE::HANGTIME))
return false;
lc::LC control(m_slot->m_control);
control.setMACPDUOpcode(opcode);
control.setMACPartition(P2_MAC_MCO_PARTITION::UNIQUE);
control.setP2DUID((uint8_t)P2_DUID::SACCH_UNSCRAMBLED);
control.setP2ScrambleOffset(m_slot->m_netScrambleOffset);
uint8_t burst[P25_P2_FRAME_LENGTH_BYTES] = { 0U };
control.encodeVCH_MACPDU(burst, false);
return m_slot->addFrame(burst, sizeof(burst), P2_DUID::SACCH_UNSCRAMBLED, true, imm);
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Initializes a Phase 2 voice processor. */
Voice::Voice(Slot* slot, bool debug, bool verbose) :
m_slot(slot),
m_rfFrames(0U),
m_netFrames(0U),
m_rfESSBCount(0U),
m_netESSBCount(0U),
m_rfESSA(false),
m_rfESSComplete(false),
m_netESSA(false),
m_netESSComplete(false),
m_debug(debug),
m_verbose(verbose)
{
::memset(m_rfESS, 0x00U, sizeof(m_rfESS));
::memset(m_rfESSB, 0x00U, sizeof(m_rfESSB));
::memset(m_netESS, 0x00U, sizeof(m_netESS));
::memset(m_netESSB, 0x00U, sizeof(m_netESSB));
}
/* Validates a host-side Phase 2 burst. */
bool Voice::validFrame(const uint8_t* data, uint32_t length) const
{
return data != nullptr && length == P25_P2_FRAME_LENGTH_BYTES;
}
/* Collects one ESS-B fragment from a 4V voice burst. */
bool Voice::processESS4V(const uint8_t* burst, uint32_t burstNo, bool net)
{
if (burst == nullptr || burstNo >= 4U)
return false;
uint8_t* ess = net ? m_netESS : m_rfESS;
bool* essB = net ? m_netESSB : m_rfESSB;
for (uint32_t bit = 0U; bit < 24U; bit++)
WRITE_BIT(ess, burstNo * 24U + bit, READ_BIT(burst, 148U + bit));
essB[burstNo] = true;
return decodeESS(net);
}
/* Collects ESS-A from a 2V voice burst. */
bool Voice::processESS2V(const uint8_t* burst, bool net)
{
if (burst == nullptr)
return false;
uint8_t* ess = net ? m_netESS : m_rfESS;
for (uint32_t bit = 0U; bit < 168U; bit++)
WRITE_BIT(ess, 96U + bit, READ_BIT(burst, 148U + bit));
if (net)
m_netESSA = true;
else
m_rfESSA = true;
return decodeESS(net);
}
/* Decodes a complete ESS codeword. */
bool Voice::decodeESS(bool net)
{
bool essA = net ? m_netESSA : m_rfESSA;
bool* essB = net ? m_netESSB : m_rfESSB;
if (!essA || !essB[0U] || !essB[1U] || !essB[2U] || !essB[3U])
return false;
uint8_t* ess = net ? m_netESS : m_rfESS;
int8_t errors = -1;
const bool wasComplete = net ? m_netESSComplete : m_rfESSComplete;
bool complete = m_essRS.decode441629(ess, &errors);
if (complete && !wasComplete && m_verbose) {
LogInfoEx(net ? LOG_NET : LOG_RF, "P25 Phase 2 Slot %u, %s ESS, algId = $%02X, keyId = $%04X, corrected = %d",
m_slot->m_slotNo + 1U, net ? "network" : "RF", ess[0U], (uint16_t)((ess[1U] << 8U) | ess[2U]), errors);
}
else if (!complete && m_debug) {
LogDebugEx(LOG_P25, "Voice::decodeESS()", "P25 Phase 2 Slot %u, %s ESS decode failed", m_slot->m_slotNo + 1U,
net ? "network" : "RF");
}
if (net)
m_netESSComplete = complete;
else
m_rfESSComplete = complete;
return complete;
}

@ -0,0 +1,172 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Modem Host Software
* 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
*
*/
/**
* @file Voice.h
* @ingroup host_p25
* @file Voice.cpp
* @ingroup host_p25
*/
#if !defined(__P25_P2_VOICE_H__)
#define __P25_P2_VOICE_H__
#include "Defines.h"
#include "common/edac/AMBE2FEC.h"
#include "common/edac/RS634717.h"
#include "common/p25/P25Defines.h"
#include "common/p25/lc/LC.h"
#include <cstdint>
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
class HostTestHooks;
namespace p25
{
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
namespace phase2 { class HOST_SW_API Slot; }
namespace packet
{
namespace phase2
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Processes P25 Phase 2 voice bursts without modem PHY logic.
* @ingroup host_p25
*/
class HOST_SW_API Voice
{
public:
/**
* @brief Resets RF voice counters.
*/
void resetRF();
/**
* @brief Resets network voice counters.
*/
void resetNet();
/** @name Frame Processing */
/**
* @brief Process a data frame from the RF interface.
* @param data Buffer containing data frame.
* @param len Length of data frame.
* @returns bool True, if data frame is processed, otherwise false.
*/
bool process(uint8_t* data, uint32_t len);
/**
* @brief Process a data frame from the network.
* @param data Buffer containing data frame.
* @param len Length of data frame.
* @returns bool True, if data frame is processed, otherwise false.
*/
bool processNetwork(uint8_t* data, uint32_t len);
/**
* @brief Builds and queues an outbound SACCH voice-channel-user PDU.
* @param opcode MAC_ACTIVE or MAC_HANGTIME PDU opcode.
* @param imm Flag indicating whether the PDU uses the immediate queue.
* @returns bool True if the logical SACCH burst was queued.
* @note The modem selects the physical SACCH opportunity.
*/
bool writeVoiceLC(uint8_t opcode, bool imm = false);
/** @} */
/** @name Statistics */
/**
* @brief Returns the number of RF bursts processed.
* @returns uint32_t The number of RF bursts processed.
*/
uint32_t getRFFrames() const { return m_rfFrames; }
/**
* @brief Returns the number of network bursts processed.
* @returns uint32_t The number of network bursts processed.
*/
uint32_t getNetFrames() const { return m_netFrames; }
/** @} */
private:
friend class p25::phase2::Slot;
friend class ::HostTestHooks;
p25::phase2::Slot* m_slot;
uint32_t m_rfFrames;
uint32_t m_netFrames;
uint32_t m_rfESSBCount;
uint32_t m_netESSBCount;
uint8_t m_rfESS[44U];
bool m_rfESSB[4U];
bool m_rfESSA;
bool m_rfESSComplete;
uint8_t m_netESS[44U];
bool m_netESSB[4U];
bool m_netESSA;
bool m_netESSComplete;
bool m_debug;
bool m_verbose;
edac::AMBE2FEC m_ambe2FEC;
edac::RS634717 m_essRS;
/**
* @brief Initializes a new instance of the Voice class.
* @param slot Instance of the Slot class.
* @param debug Flag indicating whether P25 debug is enabled.
* @param verbose Flag indicating whether P25 verbose logging is enabled.
*/
Voice(p25::phase2::Slot* slot, bool debug, bool verbose);
/**
* @brief Validates the logical Phase 2 burst length.
* @param data Buffer containing the logical burst.
* @param length Length of the logical burst.
* @returns bool True if the burst has the expected host-side shape.
*/
bool validFrame(const uint8_t* data, uint32_t length) const;
/**
* @brief Collects and decodes ESS carried by a 4V voice burst.
* @param burst Buffer containing the 4V voice burst.
* @param burstNo The burst number within the voice frame.
* @param net Flag indicating whether the burst is from the network.
* @returns bool True if the ESS was successfully processed.
*/
bool processESS4V(const uint8_t* burst, uint32_t burstNo, bool net);
/**
* @brief Collects and decodes ESS carried by a 2V voice burst.
* @param burst Buffer containing the 2V voice burst.
* @param net Flag indicating whether the burst is from the network.
* @returns bool True if the ESS was successfully processed.
*/
bool processESS2V(const uint8_t* burst, bool net);
/**
* @brief Decodes a complete RF or network ESS codeword.
* @param net Flag indicating whether the ESS is from the network.
* @returns bool True if the ESS was successfully decoded.
*/
bool decodeESS(bool net);
};
} // namespace phase2
} // namespace packet
} // namespace p25
#endif // __P25_P2_VOICE_H__

@ -0,0 +1,294 @@
// SPDX-License-Identifier: GPL-2.0-only
/**
* Digital Voice Modem - Modem Host Software
* 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 "Defines.h"
#include "common/Log.h"
#include "p25/phase2/Control.h"
#include <cassert>
using namespace p25::phase2;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a P25 Phase 2 controller. */
Control::Control(bool authoritative, uint32_t callHang, uint32_t timeout, uint32_t tgHang,
modem::Modem* modem, network::Network* network, lookups::P25AffiliationLookup* affiliations,
::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup,
uint32_t queueSize, bool debug, bool verbose) :
m_modem(modem),
m_network(network),
m_slot1(nullptr),
m_slot2(nullptr),
m_ridLookup(ridLookup),
m_tidLookup(tidLookup),
m_affiliations(affiliations),
m_callHang(callHang),
m_timeout(timeout),
m_tgHang(tgHang),
m_debug(debug),
m_verbose(verbose)
{
Slot::init(this, authoritative, callHang, modem, network, affiliations, ridLookup, tidLookup);
m_slot1 = new Slot(0U, timeout, tgHang, queueSize, debug, verbose);
m_slot2 = new Slot(1U, timeout, tgHang, queueSize, debug, verbose);
}
/* Finalizes a P25 Phase 2 controller. */
Control::~Control()
{
delete m_slot2;
delete m_slot1;
}
/* Resets controller modem queues and slot state. */
void Control::reset()
{
if (m_modem != nullptr) {
m_modem->clearP25P2Frame1();
m_modem->clearP25P2Frame2();
}
m_slot1->reset();
m_slot2->reset();
}
/* Processes one RF frame for a slot. */
bool Control::processFrame(uint32_t slotNo, uint8_t* data, uint32_t length)
{
switch (slotNo) {
case 0U:
return m_slot1->processFrame(data, length);
case 1U:
return m_slot2->processFrame(data, length);
default:
LogError(LOG_P25, "P25 Phase 2, invalid RF slot, slotNo = %u", slotNo + 1U);
return false;
}
}
/* Returns the next queued frame length for a slot. */
uint32_t Control::peekFrameLength(uint32_t slotNo)
{
switch (slotNo) {
case 0U:
return m_slot1->peekFrameLength();
case 1U:
return m_slot2->peekFrameLength();
default:
LogError(LOG_P25, "P25 Phase 2, invalid queue slot, slotNo = %u", slotNo + 1U);
return 0U;
}
}
/* Returns whether a slot transmit queue is full. */
bool Control::isQueueFull(uint32_t slotNo)
{
switch (slotNo) {
case 0U:
return m_slot1->isQueueFull();
case 1U:
return m_slot2->isQueueFull();
default:
LogError(LOG_P25, "P25 Phase 2, invalid queue slot, slotNo = %u", slotNo + 1U);
return true;
}
}
/* Retrieves the next queued frame for a slot. */
uint32_t Control::getFrame(uint32_t slotNo, uint8_t* data, bool* imm)
{
switch (slotNo) {
case 0U:
return m_slot1->getFrame(data, imm);
case 1U:
return m_slot2->getFrame(data, imm);
default:
LogError(LOG_P25, "P25 Phase 2, invalid queue slot, slotNo = %u", slotNo + 1U);
return 0U;
}
}
/* Advances controller and slot timers. */
void Control::clock(uint32_t ms)
{
if (m_network != nullptr)
processNetwork();
m_slot1->clock(ms);
m_slot2->clock(ms);
}
/* Returns a Phase 2 slot. */
Slot& Control::slot(uint32_t slotNo)
{
assert(slotNo < SLOT_COUNT);
return slotNo == 0U ? *m_slot1 : *m_slot2;
}
/* Returns a constant Phase 2 slot. */
const Slot& Control::slot(uint32_t slotNo) const
{
assert(slotNo < SLOT_COUNT);
return slotNo == 0U ? *m_slot1 : *m_slot2;
}
/* Returns the RF state for a slot. */
RPT_RF_STATE Control::getRFState(uint32_t slotNo) const
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid RF state slot, slotNo = %u", slotNo + 1U);
return RS_RF_INVALID;
}
return slot(slotNo).getRFState();
}
/* Returns the network state for a slot. */
RPT_NET_STATE Control::getNetState(uint32_t slotNo) const
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid network state slot, slotNo = %u", slotNo + 1U);
return RS_NET_IDLE;
}
return slot(slotNo).getNetState();
}
/* Clears a rejected RF state. */
void Control::clearRFReject(uint32_t slotNo)
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid RF reject slot, slotNo = %u", slotNo + 1U);
return;
}
slot(slotNo).clearRFReject();
}
/* Returns whether either slot is busy. */
bool Control::isBusy() const
{
return m_slot1->isBusy() || m_slot2->isBusy();
}
/* Permits a destination on a non-authoritative slot. */
void Control::permittedTG(uint32_t dstId, uint32_t slotNo)
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid permit slot, slotNo = %u", slotNo + 1U);
return;
}
slot(slotNo).permittedTG(dstId);
}
/* Touches a slot grant. */
void Control::touchGrantTG(uint32_t dstId, uint32_t slotNo)
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid grant touch slot, slotNo = %u", slotNo + 1U);
return;
}
slot(slotNo).touchGrantTG(dstId);
}
/* Releases a slot grant. */
void Control::releaseGrantTG(uint32_t dstId, uint32_t slotNo)
{
if (slotNo >= SLOT_COUNT) {
LogError(LOG_P25, "P25 Phase 2, invalid grant release slot, slotNo = %u", slotNo + 1U);
return;
}
slot(slotNo).releaseGrantTG(dstId);
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Processes the next network Phase 2 frame. */
void Control::processNetwork()
{
if (m_network == nullptr || !m_network->hasP25P2Data())
return;
bool ret = false;
uint32_t length = 0U;
UInt8Array buffer = m_network->readP25P2(ret, length);
if (!ret || buffer == nullptr) {
if (m_debug)
LogDebugEx(LOG_NET, "Control::processNetwork()", "P25 Phase 2 network read returned no frame");
return;
}
if (length < (24U + P25DEF::P25_P2_FRAME_LENGTH_BYTES)) {
LogWarning(LOG_NET, "P25 Phase 2, malformed network frame, length = %u, minimum = %u", length, 24U + P25DEF::P25_P2_FRAME_LENGTH_BYTES);
return;
}
// The network header carries the slot flag and DUID in byte 19. The logical
// 40-byte burst begins at byte 24; physical TDMA scheduling is modem-owned.
uint32_t slotNo = (buffer[19U] & 0x80U) != 0U ? 1U : 0U;
lc::LC control;
control.setLCO(buffer[4U]);
uint32_t srcId = (buffer[5U] << 16) | (buffer[6U] << 8) | buffer[7U];
uint32_t dstId = (buffer[8U] << 16) | (buffer[9U] << 8) | buffer[10U];
control.setSrcId(srcId);
control.setDstId(dstId);
control.setMFId(buffer[15U]);
// forward onto the specific slot for final processing and delivery
bool processed = false;
switch (slotNo) {
case 0U:
processed = m_slot1->processNetwork(buffer.get() + 24U, P25DEF::P25_P2_FRAME_LENGTH_BYTES, control,
(P25DEF::P2_DUID::E)(buffer[19U] & 0x7FU), buffer[14U]);
break;
case 1U:
processed = m_slot2->processNetwork(buffer.get() + 24U, P25DEF::P25_P2_FRAME_LENGTH_BYTES, control,
(P25DEF::P2_DUID::E)(buffer[19U] & 0x7FU), buffer[14U]);
break;
default:
break;
}
if (!processed && m_debug)
LogDebugEx(LOG_NET, "Control::processNetwork()", "P25 Phase 2 Slot %u, network frame rejected, srcId = %u, dstId = %u, duid = $%02X",
slotNo + 1U, srcId, dstId, buffer[19U] & 0x7FU);
}
/* Writes an RF frame to the Phase 2 network stream. */
bool Control::writeNetwork(Slot* slot, const uint8_t* data, uint32_t length)
{
if (m_network == nullptr || slot == nullptr || data == nullptr)
return true;
const bool ret = m_network->writeP25P2(slot->m_control, slot->m_duid, (uint8_t)slot->m_slotNo, data, slot->m_controlByte);
if (!ret)
LogWarning(LOG_NET, "P25 Phase 2 Slot %u, failed to write network frame, duid = $%02X",
slot->m_slotNo + 1U, (uint8_t)slot->m_duid);
return ret;
}

@ -0,0 +1,217 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Modem Host Software
* 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
*
*/
/**
* @file Control.h
* @ingroup host_p25
* @file Control.cpp
* @ingroup host_p25
*/
#if !defined(__P25_P2_CONTROL_H__)
#define __P25_P2_CONTROL_H__
#include "Defines.h"
#include "common/network/Network.h"
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "p25/phase2/Slot.h"
#include "p25/lookups/P25AffiliationLookup.h"
#include "modem/Modem.h"
#include <cstdint>
class HostTestHooks;
namespace p25
{
namespace phase2
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements core controller logic for handling P25 Phase 2.
* @ingroup host_p25
*/
class HOST_SW_API Control
{
public:
/** @brief Number of P25 Phase 2 TDMA slots. */
static constexpr uint32_t SLOT_COUNT = 2U;
/**
* @brief Initializes a new instance of the Control class.
* @param authoritative Flag indicating whether or not the DVM is grant authoritative.
* @param callHang Amount of hangtime for a P25 Phase 2 call.
* @param timeout Transmit timeout.
* @param tgHang Amount of time to hang on the last talkgroup mode from RF.
* @param modem Instance of the Modem class.
* @param network Instance of the BaseNetwork class.
* @param affiliations Instance of the P25AffiliationLookup class.
* @param ridLookup Instance of the RadioIdLookup class.
* @param tidLookup Instance of the TalkgroupRulesLookup class.
* @param queueSize Modem frame buffer queue size (bytes).
* @param debug Flag indicating whether P25 debug is enabled.
* @param verbose Flag indicating whether P25 verbose logging is enabled.
*/
Control(bool authoritative, uint32_t callHang, uint32_t timeout, uint32_t tgHang,
modem::Modem* modem, network::Network* network, lookups::P25AffiliationLookup* affiliations,
::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup,
uint32_t queueSize, bool debug, bool verbose);
/**
* @brief Finalizes a P25 Phase 2 controller.
*/
~Control();
/** @brief Resets both Phase 2 slots and modem queues. */
void reset();
/** @name Frame Processing */
/**
* @brief Process a data frame from the RF interface.
* @param slotNo P25 Phase 2 slot number.
* @param data Buffer containing data frame.
* @param len Length of data frame.
* @returns bool True, if frame was successfully processed, otherwise false.
*/
bool processFrame(uint32_t slotNo, uint8_t* data, uint32_t len);
/**
* @brief Get the frame data length for the next frame in the slot queue.
* @param slotNo P25 Phase 2 slot number.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t peekFrameLength(uint32_t slotNo);
/**
* @brief Helper to determine whether or not the slot queue is full.
* @param slotNo P25 Phase 2 slot number.
* @returns bool True if the slot queue is full, otherwise false.
*/
bool isQueueFull(uint32_t slotNo);
/**
* @brief Get frame data from the slot queue.
* @param slotNo P25 Phase 2 slot number.
* @param[out] data Buffer to store frame data.
* @param[out] imm Flag indicating whether the frame is immediate.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t getFrame(uint32_t slotNo, uint8_t* data, bool* imm = nullptr);
/** @} */
/** @name Data Clocking */
/**
* @brief Updates the processor.
* @param ms Number of milliseconds.
*/
void clock(uint32_t ms);
/** @} */
/**
* @brief Gets a P25 Phase 2 slot instance.
* @param slotNo P25 Phase 2 slot number.
* @returns Slot& Slot instance.
*/
Slot& slot(uint32_t slotNo);
/**
* @brief Gets a constant P25 Phase 2 slot instance.
* @param slotNo P25 Phase 2 slot number.
* @returns const Slot& Slot instance.
*/
const Slot& slot(uint32_t slotNo) const;
/**
* @brief Returns the RF state for a slot, or RS_RF_INVALID.
* @param slotNo P25 Phase 2 slot number.
* @returns RPT_RF_STATE RF state for the specified slot, or RS_RF_INVALID.
*/
RPT_RF_STATE getRFState(uint32_t slotNo) const;
/**
* @brief Returns the network state for a slot.
* @param slotNo P25 Phase 2 slot number.
* @returns RPT_NET_STATE Network state for the specified slot.
*/
RPT_NET_STATE getNetState(uint32_t slotNo) const;
/**
* @brief Clears a rejected RF state for a slot.
* @param slotNo P25 Phase 2 slot number.
*/
void clearRFReject(uint32_t slotNo);
/**
* @brief Returns whether either Phase 2 slot is busy.
* @returns bool True if either slot is busy, false otherwise.
*/
bool isBusy() const;
/** @name Supervisory Control */
/**
* @brief Permits a destination on a non-authoritative Phase 2 slot.
* @param dstId Destination ID.
* @param slotNo P25 Phase 2 slot number.
*/
void permittedTG(uint32_t dstId, uint32_t slotNo);
/**
* @brief Touches a CC grant associated with a Phase 2 slot.
* @param dstId Destination ID.
* @param slotNo P25 Phase 2 slot number.
*/
void touchGrantTG(uint32_t dstId, uint32_t slotNo);
/**
* @brief Releases a CC grant associated with a Phase 2 slot.
* @param dstId Destination ID.
* @param slotNo P25 Phase 2 slot number.
*/
void releaseGrantTG(uint32_t dstId, uint32_t slotNo);
/** @} */
private:
friend class Slot;
friend class packet::phase2::Voice;
friend class ::HostTestHooks;
/* Interfaces */
modem::Modem* m_modem;
network::Network* m_network;
/* TDMA Slots */
Slot* m_slot1;
Slot* m_slot2;
/* Access and Grant Lookups */
::lookups::RadioIdLookup* m_ridLookup;
::lookups::TalkgroupRulesLookup* m_tidLookup;
lookups::P25AffiliationLookup* m_affiliations;
/* Call Timing */
uint32_t m_callHang;
uint32_t m_timeout;
uint32_t m_tgHang;
/* Logging */
bool m_debug;
bool m_verbose;
/**
* @brief Process a data frame from the network.
* @returns bool True, if frame was successfully processed, otherwise false.
*/
void processNetwork();
/**
* @brief Writes an RF frame to the Phase 2 network stream.
* @param slot Slot that owns the frame.
* @param data Buffer containing the frame data.
* @param len Length of the frame data.
* @returns bool True if the frame was written, otherwise false.
*/
bool writeNetwork(Slot* slot, const uint8_t* data, uint32_t len);
};
} // namespace phase2
} // namespace p25
#endif // __P25_P2_CONTROL_H__

File diff suppressed because it is too large Load Diff

@ -0,0 +1,372 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Modem Host Software
* 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
*
*/
/**
* @file Slot.h
* @ingroup host_p25
* @file Slot.cpp
* @ingroup host_p25
*/
#if !defined(__P25_P2_SLOT_H__)
#define __P25_P2_SLOT_H__
#include "Defines.h"
#include "common/p25/P25Defines.h"
#include "common/p25/lc/LC.h"
#include "common/network/Network.h"
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/RingBuffer.h"
#include "common/Timer.h"
#include "p25/packet/phase2/Voice.h"
#include "modem/Modem.h"
#include <cstdint>
#include <mutex>
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
class HostTestHooks;
namespace p25
{
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
namespace phase2 { class HOST_SW_API Control; }
namespace lookups { class HOST_SW_API P25AffiliationLookup; }
namespace phase2
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements one P25 Phase 2 TDMA slot.
* @ingroup host_p25
*/
class HOST_SW_API Slot
{
public:
/**
* @brief Initializes a new instance of the Slot class.
* @param slotNo P25 Phase 2 slot number.
* @param timeout RF/network transmit timeout in seconds.
* @param tgHang Talkgroup hang time in seconds.
* @param queueSize Slot queue size in bytes.
* @param debug Enable debug logging.
* @param verbose Enable verbose logging.
*/
Slot(uint32_t slotNo, uint32_t timeout, uint32_t tgHang, uint32_t queueSize, bool debug, bool verbose);
/** @brief Resets RF/network state, statistics, and queued frames. */
void reset();
/** @name Frame Processing */
/**
* @brief Process a data frame from the RF interface.
* @param data Buffer containing data frame.
* @param len Length of data frame.
* @returns bool True, if frame was successfully processed, otherwise false.
*/
bool processFrame(uint8_t* data, uint32_t len);
/**
* @brief Get the frame data length for the next frame in the slot queue.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t peekFrameLength();
/**
* @brief Helper to determine whether or not the slot queue is full.
* @returns bool True if the slot queue is full, otherwise false.
*/
bool isQueueFull() const;
/**
* @brief Get frame data from the slot queue.
* @param[out] data Buffer to store frame data.
* @param[out] imm Flag indicating whether the frame is immediate.
* @returns uint32_t Length of frame data retrieved.
*/
uint32_t getFrame(uint8_t* data, bool* imm = nullptr);
/**
* @brief Process a data frame from the network.
* @param data Buffer containing data frame.
* @param len Length of data frame.
* @param control Decoded Phase 2 link control.
* @param duid Phase 2 data unit ID.
* @param controlByte DVM network control byte.
* @returns bool True, if frame was successfully processed, otherwise false.
*/
bool processNetwork(uint8_t* data, uint32_t len, const lc::LC& control, defines::P2_DUID::E duid,
uint8_t controlByte);
/** @} */
/** @name Data Clocking */
/**
* @brief Updates the processor.
* @param ms Number of milliseconds.
*/
void clock(uint32_t ms);
/** @} */
/** @name Supervisory Control */
/**
* @brief Permits a TGID on a non-authoritative host.
* @param dstId Destination ID.
*/
void permittedTG(uint32_t dstId);
/**
* @brief Touches an active grant to keep its CC timer alive.
* @param dstId Destination ID.
*/
void touchGrantTG(uint32_t dstId);
/**
* @brief Releases an active grant.
* @param dstId Destination ID.
*/
void releaseGrantTG(uint32_t dstId);
/**
* @brief Clears a rejected RF call and returns the slot to listening.
*/
void clearRFReject();
/** @} */
/**
* @brief Gets the P25 Phase 2 slot number.
* @returns uint32_t P25 Phase 2 slot number.
*/
uint32_t getSlotNo() const { return m_slotNo; }
/**
* @brief Returns the current RF state.
* @returns RPT_RF_STATE Current RF state.
*/
RPT_RF_STATE getRFState() const { return m_rfState; }
/**
* @brief Returns the current network state.
* @returns RPT_NET_STATE Current network state.
*/
RPT_NET_STATE getNetState() const { return m_netState; }
/**
* @brief Returns whether RF or network traffic owns the slot.
* @returns bool True if the slot is busy, false otherwise.
*/
bool isBusy() const { return m_rfState != RS_RF_LISTENING || m_netState != RS_NET_IDLE; }
/**
* @brief Helper to initialize shared P25 Phase 2 slot configuration.
* @param p2 Instance of the Control class.
* @param authoritative Flag indicating whether or not the DVM is grant authoritative.
* @param callHang Amount of hangtime for a P25 Phase 2 call.
* @param modem Instance of the Modem class.
* @param network Instance of the Network class.
* @param affiliations Instance of the P25AffiliationLookup class.
* @param ridLookup Instance of the RadioIdLookup class.
* @param tidLookup Instance of the TalkgroupRulesLookup class.
*/
static void init(Control* control, bool authoritative, uint32_t callHang, modem::Modem* modem, network::Network* network,
lookups::P25AffiliationLookup* affiliations, ::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup);
private:
friend class ::HostTestHooks;
friend class phase2::Control;
friend class packet::phase2::Voice;
packet::phase2::Voice m_voice;
uint32_t m_slotNo;
RingBuffer<uint8_t> m_txQueue;
RingBuffer<uint8_t> m_txImmQueue;
mutable std::mutex m_queueLock;
RPT_RF_STATE m_rfState;
uint32_t m_rfLastDstId;
uint32_t m_rfLastSrcId;
RPT_NET_STATE m_netState;
uint32_t m_netLastDstId;
uint32_t m_netLastSrcId;
uint32_t m_permittedDstId;
lc::LC m_control;
public:
/**
* @brief Host-side VCH lifecycle. Physical LCCH/ISCH scheduling remains in the modem.
*/
enum class VCH_STATE : uint8_t {
IDLE,
PTT,
ACTIVE,
HANGTIME,
TERMINATING
};
private:
bool m_rfResetPending;
bool m_netResetPending;
VCH_STATE m_rfVCHState;
VCH_STATE m_netVCHState;
uint8_t m_rfPTTCount;
uint8_t m_rfEndPTTCount;
uint8_t m_netEndPTTCount;
uint32_t m_rfFrames;
uint32_t m_netFrames;
uint32_t m_netLost;
uint32_t m_netMissed;
uint32_t m_rfBits;
uint32_t m_netBits;
uint32_t m_rfErrs;
uint32_t m_netErrs;
bool m_rfTimeout;
bool m_netTimeout;
uint8_t m_frameLossCnt;
uint8_t m_frameLossThreshold;
uint32_t m_elapsedMs;
uint32_t m_vcuElapsedMs;
Timer m_rfTimeoutTimer;
Timer m_rfTGHang;
Timer m_netTimeoutTimer;
Timer m_netTGHang;
Timer m_networkWatchdog;
Timer m_rfLossWatchdog;
Timer m_rfCallHangTimer;
Timer m_netCallHangTimer;
bool m_debug;
bool m_verbose;
defines::P2_DUID::E m_duid;
uint8_t m_controlByte;
uint32_t m_rfBurstCount;
uint32_t m_netBurstCount;
uint16_t m_rfScrambleOffset;
uint16_t m_netScrambleOffset;
static Control* s_control;
static bool s_authoritative;
static uint32_t s_callHang;
static modem::Modem* s_modem;
static network::Network* s_network;
static lookups::P25AffiliationLookup* s_affiliations;
static ::lookups::RadioIdLookup* s_ridLookup;
static ::lookups::TalkgroupRulesLookup* s_tidLookup;
/**
* @brief Adds a frame to the slot queue.
* @param data Buffer containing frame data.
* @param net Flag indicating whether the frame is from the network or RF.
* @returns bool True, if frame was successfully added to the queue, otherwise false.
*/
bool addFrame(const uint8_t* data, uint32_t len, defines::P2_DUID::E duid,
bool net = false, bool imm = false);
/**
* @brief Resets RF call state and timers without resetting network state.
*/
void resetRF();
/**
* @brief Resets network call state and timers without resetting RF state.
*/
void resetNet();
/**
* @brief Validates RF call ownership and access-control policy.
* @param srcId Source ID of the RF call.
* @param dstId Destination ID of the RF call.
* @param group Flag indicating whether the call is a group call.
* @returns bool True if the RF call is valid and allowed by access-control policy.
*/
bool validateRFCall(uint32_t srcId, uint32_t dstId, bool group);
/**
* @brief Touches the grant associated with accepted traffic.
* @param dstId Destination ID of the call whose grant is being touched.
*/
void touchGrant(uint32_t dstId);
/**
* @brief Helper to process loss of frame stream from modem.
* @param type Type of RF frame loss.
*/
void processFrameLoss(RPT_RF_LOSS_TYPE type);
/**
* @brief Begins message-trunking hangtime for an RF or network call.
* @param net Flag indicating whether the hangtime is for the network or RF call.
*/
void beginHangtime(bool net);
/**
* @brief Queues the two-burst call termination sequence.
* @param net Flag indicating whether the termination is for the network or RF call.
*/
void writeEnd(bool net);
/**
* @brief Encodes and queues one outbound logical MAC PDU.
* @note Physical LCCH, ISCH, slot, and superframe scheduling is modem-owned.
*/
bool queueMACPDU(uint8_t opcode, bool imm = false);
/**
* @brief Processes and forwards one logical FACCH or SACCH burst.
* @param data Buffer containing the logical MAC-bearing burst.
* @param len Length of the logical burst.
* @param duid Phase 2 data unit ID.
* @param net Flag indicating that the burst originated from the network.
* @param[out] release Flag set when the MAC message releases the current call.
* @returns bool True if the MAC burst was successfully processed.
*/
bool processMAC(uint8_t* data, uint32_t len, defines::P2_DUID::E duid,
bool net, bool& release);
/**
* @brief Returns whether a DUID identifies a voice traffic burst.
* @param duid The DUID to check.
* @returns bool True if the DUID identifies a voice traffic burst, otherwise false.
*/
static bool isVoiceDUID(defines::P2_DUID::E duid);
/**
* @brief Returns whether a DUID identifies a VCH FACCH burst.
* @param duid The DUID to check.
* @returns bool True if the DUID identifies a VCH FACCH burst, otherwise false.
*/
static bool isFACCHDUID(defines::P2_DUID::E duid);
/**
* @brief Returns whether a DUID identifies a VCH SACCH burst.
* @param duid The DUID to check.
* @returns bool True if the DUID identifies a VCH SACCH burst, otherwise false.
*/
static bool isSACCHDUID(defines::P2_DUID::E duid);
/**
* @brief Returns whether a DUID identifies an LCCH burst.
* @param duid The DUID to check.
* @returns bool True if the DUID identifies an LCCH burst, otherwise false.
*/
static bool isLCCHDUID(defines::P2_DUID::E duid);
};
} // namespace phase2
} // namespace p25
#endif // __P25_P2_SLOT_H__

@ -0,0 +1,139 @@
// 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 "common/edac/AMBE2FEC.h"
#include "common/edac/Golay24128.h"
#include <catch2/catch_test_macros.hpp>
#include <cstring>
using namespace edac;
TEST_CASE("AMBE2FEC Annex S interleave round trip", "[edac][ambe2][p25][p2]")
{
AMBE2FEC fec;
uint8_t codeword[9U] = {0xA5U, 0x5AU, 0x13U, 0xC7U, 0xE1U, 0x42U, 0x89U, 0x3CU, 0x80U};
uint8_t frame[9U] = {0U};
uint8_t decoded[9U] = {0U};
fec.interleave(frame, codeword);
fec.deinterleave(decoded, frame);
REQUIRE(std::memcmp(decoded, codeword, sizeof(codeword)) == 0);
}
TEST_CASE("AMBE2FEC corrects the half-rate Golay codewords", "[edac][ambe2][p25][p2]")
{
AMBE2FEC fec;
uint8_t codeword[9U] = {0U};
uint8_t frame[9U] = {0U};
uint8_t decoded[9U] = {0U};
fec.encode(codeword, 0xA5BU, 0x5A3U, 0x3A5U, 0x2D37U);
fec.interleave(frame, codeword);
fec.deinterleave(decoded, frame);
decoded[0U] ^= 0x01U;
decoded[3U] ^= 0x04U;
REQUIRE(fec.regenerate(decoded) > 0U);
for (uint32_t i = 0U; i < sizeof(codeword); i++)
REQUIRE(decoded[i] == codeword[i]);
}
TEST_CASE("AMBE2FEC extracts and repairs 4V/2V burst voice fields", "[edac][ambe2][p25][p2]")
{
AMBE2FEC fec;
uint8_t burst[40U] = {0U};
uint8_t frame[9U] = {0U};
uint8_t codeword[9U] = {0U};
fec.encode(codeword, 0xA5BU, 0x5A3U, 0x3A5U, 0x2D37U);
fec.interleave(frame, codeword);
for (uint32_t bit = 0U; bit < 72U; bit++)
burst[(2U + bit) >> 3U] |= static_cast<uint8_t>(((frame[bit >> 3U] >> (7U - (bit & 7U))) & 1U) << (7U - ((2U + bit) & 7U)));
fec.encode(codeword, 0x321U, 0x654U, 0x2AAU, 0x1555U);
fec.interleave(frame, codeword);
for (uint32_t bit = 0U; bit < 72U; bit++)
burst[(76U + bit) >> 3U] |= static_cast<uint8_t>(((frame[bit >> 3U] >> (7U - (bit & 7U))) & 1U) << (7U - ((76U + bit) & 7U)));
uint8_t cleanBurst[40U] = {0U};
::memcpy(cleanBurst, burst, sizeof(cleanBurst));
uint8_t roundTrip[40U] = {0U};
::memcpy(roundTrip, burst, sizeof(roundTrip));
fec.regenerateBurst(roundTrip, false, false);
REQUIRE(std::memcmp(roundTrip, cleanBurst, sizeof(roundTrip)) == 0);
burst[1U] ^= 0x20U;
burst[10U] ^= 0x08U;
fec.regenerateBurst(burst, false, false);
for (uint32_t i = 0U; i < sizeof(burst); i++) {
INFO("byte " << i);
REQUIRE(burst[i] == cleanBurst[i]);
}
}
TEST_CASE("AMBE2FEC extracts inbound 4V/2V voice fields", "[edac][ambe2][p25][p2]")
{
AMBE2FEC fec;
uint8_t burst[40U] = {0U};
uint8_t frame[9U] = {0U};
uint8_t codeword[9U] = {0U};
fec.encode(codeword, 0x6A5U, 0x321U, 0x155U, 0x2AAAU);
fec.interleave(frame, codeword);
for (uint32_t base : {0U, 74U}) {
for (uint32_t bit = 0U; bit < 72U; bit++) {
uint32_t source = (frame[bit >> 3U] >> (7U - (bit & 7U))) & 1U;
uint32_t target = base + bit;
burst[target >> 3U] |= static_cast<uint8_t>(source << (7U - (target & 7U)));
}
}
uint8_t cleanBurst[40U] = {0U};
::memcpy(cleanBurst, burst, sizeof(cleanBurst));
burst[2U] ^= 0x04U;
fec.regenerateBurst(burst, true, false);
REQUIRE(std::memcmp(burst, cleanBurst, sizeof(burst)) == 0);
}
TEST_CASE("AMBE2FEC repairs all four 4V voice codewords", "[edac][ambe2][p25][p2]")
{
AMBE2FEC fec;
uint8_t burst[40U] = {0U};
const uint32_t offsets[4U] = {2U, 76U, 176U, 250U};
const uint16_t values[4U][4U] = {
{0xA5BU, 0x5A3U, 0x3A5U, 0x2D37U},
{0x321U, 0x654U, 0x2AAU, 0x1555U},
{0x777U, 0x111U, 0x155U, 0x2AAAU},
{0x12AU, 0x345U, 0x3CCU, 0x1B6DU}
};
for (uint32_t frameNo = 0U; frameNo < 4U; frameNo++) {
uint8_t codeword[9U] = {0U};
uint8_t frame[9U] = {0U};
fec.encode(codeword, values[frameNo][0U], values[frameNo][1U], values[frameNo][2U], values[frameNo][3U]);
fec.interleave(frame, codeword);
for (uint32_t bit = 0U; bit < 72U; bit++) {
uint32_t value = (frame[bit >> 3U] >> (7U - (bit & 7U))) & 1U;
uint32_t target = offsets[frameNo] + bit;
burst[target >> 3U] |= static_cast<uint8_t>(value << (7U - (target & 7U)));
}
}
uint8_t clean[40U] = {0U};
::memcpy(clean, burst, sizeof(clean));
burst[3U] ^= 0x10U;
burst[22U] ^= 0x04U;
burst[34U] ^= 0x20U;
fec.regenerateBurst(burst, false, true);
REQUIRE(std::memcmp(burst, clean, sizeof(burst)) == 0);
}

@ -0,0 +1,42 @@
// 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 "common/edac/AMBEFEC.h"
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <cstring>
using namespace edac;
TEST_CASE("AMBEFEC regenerates a clean P25 IMBE frame", "[edac][ambefec][p25]")
{
AMBEFEC fec;
std::array<uint8_t, 18U> frame{};
fec.regenerateIMBE(frame.data());
REQUIRE(fec.regenerateIMBE(frame.data()) == 0U);
REQUIRE(fec.measureP25BER(frame.data()) == 0U);
}
TEST_CASE("AMBEFEC corrects isolated P25 IMBE bit errors", "[edac][ambefec][p25]")
{
AMBEFEC fec;
std::array<uint8_t, 18U> clean{};
fec.regenerateIMBE(clean.data());
REQUIRE(fec.regenerateIMBE(clean.data()) == 0U);
for (uint32_t bit = 0U; bit < 144U; bit += 17U) {
std::array<uint8_t, 18U> frame = clean;
frame[bit >> 3U] ^= static_cast<uint8_t>(1U << (7U - (bit & 7U)));
fec.regenerateIMBE(frame.data());
REQUIRE(std::memcmp(frame.data(), clean.data(), frame.size()) == 0);
}
}

@ -163,6 +163,120 @@ bool HostTestHooks::dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_
return slot.processFrame(frame, sizeof(frame));
}
/* Gets a P25 Phase 2 slot for tests. */
p25::phase2::Slot& HostTestHooks::p25P2Slot(p25::phase2::Control& control, uint32_t slotNo)
{
assert(slotNo < p25::phase2::Control::SLOT_COUNT);
return slotNo == 0U ? *control.m_slot1 : *control.m_slot2;
}
/* Gets P25 Phase 2 RF state. */
RPT_RF_STATE HostTestHooks::p25P2RFState(const p25::phase2::Slot& slot) { return slot.m_rfState; }
void HostTestHooks::p25P2SetRFState(p25::phase2::Slot& slot, RPT_RF_STATE state) { slot.m_rfState = state; }
/* Gets P25 Phase 2 last source ID. */
uint32_t HostTestHooks::p25P2LastSrcId(const p25::phase2::Slot& slot)
{
return slot.m_rfLastSrcId != 0U ? slot.m_rfLastSrcId : slot.m_netLastSrcId;
}
/* Gets P25 Phase 2 last destination ID. */
uint32_t HostTestHooks::p25P2LastDstId(const p25::phase2::Slot& slot)
{
return slot.m_rfLastDstId != 0U ? slot.m_rfLastDstId : slot.m_netLastDstId;
}
/* Gets P25 Phase 2 RF scramble offset. */
uint16_t HostTestHooks::p25P2RFScrambleOffset(const p25::phase2::Slot& slot) { return slot.m_rfScrambleOffset; }
/* Gets P25 Phase 2 network scramble offset. */
uint16_t HostTestHooks::p25P2NetScrambleOffset(const p25::phase2::Slot& slot) { return slot.m_netScrambleOffset; }
/* Gets whether P25 Phase 2 ESS is complete. */
bool HostTestHooks::p25P2ESSComplete(const p25::phase2::Slot& slot)
{
return slot.m_voice.m_rfESSComplete || slot.m_voice.m_netESSComplete;
}
/* Gets P25 Phase 2 ESS algorithm ID. */
uint8_t HostTestHooks::p25P2ESSAlgId(const p25::phase2::Slot& slot)
{
const uint8_t* ess = slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS : slot.m_voice.m_rfESS;
return ess[0U];
}
/* Gets P25 Phase 2 ESS key ID. */
uint16_t HostTestHooks::p25P2ESSKeyId(const p25::phase2::Slot& slot)
{
const uint8_t* ess = slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS : slot.m_voice.m_rfESS;
return static_cast<uint16_t>((ess[1U] << 8U) | ess[2U]);
}
/* Copies P25 Phase 2 ESS MI. */
void HostTestHooks::p25P2ESSMI(const p25::phase2::Slot& slot, uint8_t* mi)
{
if (mi != nullptr)
::memcpy(mi, slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS + 3U :
slot.m_voice.m_rfESS + 3U, 9U);
}
/* Gets P25 Phase 2 RF VCH state. */
p25::phase2::Slot::VCH_STATE HostTestHooks::p25P2RFVCHState(const p25::phase2::Slot& slot) { return slot.m_rfVCHState; }
/* Gets P25 Phase 2 network VCH state. */
p25::phase2::Slot::VCH_STATE HostTestHooks::p25P2NetVCHState(const p25::phase2::Slot& slot) { return slot.m_netVCHState; }
/* Gets P25 Phase 2 DUID. */
p25::defines::P2_DUID::E HostTestHooks::p25P2DUID(const p25::phase2::Slot& slot) { return slot.m_duid; }
/* Gets P25 Phase 2 RF PTT count. */
uint8_t HostTestHooks::p25P2RFPTTCount(const p25::phase2::Slot& slot) { return slot.m_rfPTTCount; }
/* Gets P25 Phase 2 RF frames count. */
uint32_t HostTestHooks::p25P2RFFrames(const p25::phase2::Slot& slot) { return slot.m_rfFrames; }
/* Gets P25 Phase 2 network frames count. */
uint32_t HostTestHooks::p25P2NetFrames(const p25::phase2::Slot& slot) { return slot.m_netFrames; }
/* Gets P25 Phase 2 network lost frames count. */
uint32_t HostTestHooks::p25P2NetLost(const p25::phase2::Slot& slot) { return slot.m_netLost; }
/* Gets P25 Phase 2 network missed frames count. */
uint32_t HostTestHooks::p25P2NetMissed(const p25::phase2::Slot& slot) { return slot.m_netMissed; }
/* Gets P25 Phase 2 RF bits, network bits, RF errors, and network errors. */
uint32_t HostTestHooks::p25P2RFBits(const p25::phase2::Slot& slot) { return slot.m_rfBits; }
/* Gets P25 Phase 2 network bits. */
uint32_t HostTestHooks::p25P2NetBits(const p25::phase2::Slot& slot) { return slot.m_netBits; }
/* Gets P25 Phase 2 RF errors. */
uint32_t HostTestHooks::p25P2RFErrs(const p25::phase2::Slot& slot) { return slot.m_rfErrs; }
/* Gets P25 Phase 2 network errors. */
uint32_t HostTestHooks::p25P2NetErrs(const p25::phase2::Slot& slot) { return slot.m_netErrs; }
/* Gets P25 network state. */
RPT_NET_STATE HostTestHooks::p25NetState(const p25::Control& control) { return control.m_netState; }

@ -14,6 +14,8 @@
#include "host/dmr/Slot.h"
#include "host/nxdn/Control.h"
#include "host/p25/Control.h"
#include "host/p25/phase2/Control.h"
#include "host/p25/phase2/Slot.h"
#include "host/p25/packet/Voice.h"
// ---------------------------------------------------------------------------
@ -217,6 +219,147 @@ public:
* @param duid DUID to encode into frame NID bits.
*/
static void p25StampRFFrameNID(p25::Control& control, uint8_t* frame, p25::defines::DUID::E duid);
/**
* @brief Gets a P25 Phase 2 slot for tests.
* @param control P25 Phase 2 control instance.
* @param slotNo Slot number to retrieve.
* @return p25::phase2::Slot& Reference to the requested Phase 2 slot.
*/
static p25::phase2::Slot& p25P2Slot(p25::phase2::Control& control, uint32_t slotNo);
/**
* @brief Gets P25 Phase 2 RF state.
* @param slot P25 Phase 2 slot instance.
* @return RPT_RF_STATE Current RF state of the slot.
*/
static RPT_RF_STATE p25P2RFState(const p25::phase2::Slot& slot);
/**
* @brief Forces P25 Phase 2 RF state for recovery-path tests.
* @param slot P25 Phase 2 slot instance.
* @param state RF state to set for the slot.
*/
static void p25P2SetRFState(p25::phase2::Slot& slot, RPT_RF_STATE state);
/**
* @brief Gets P25 Phase 2 last source ID.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Last source ID of the slot.
*/
static uint32_t p25P2LastSrcId(const p25::phase2::Slot& slot);
/**
* @brief Gets P25 Phase 2 last destination ID.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Last destination ID of the slot.
*/
static uint32_t p25P2LastDstId(const p25::phase2::Slot& slot);
/**
* @brief Gets P25 Phase 2 RF scramble offset.
* @param slot P25 Phase 2 slot instance.
* @return uint16_t RF scramble offset of the slot.
*/
static uint16_t p25P2RFScrambleOffset(const p25::phase2::Slot& slot);
/**
* @brief Gets P25 Phase 2 network scramble offset.
* @param slot P25 Phase 2 slot instance.
* @return uint16_t Network scramble offset of the slot.
*/
static uint16_t p25P2NetScrambleOffset(const p25::phase2::Slot& slot);
/**
* @brief Gets whether P25 Phase 2 ESS is complete.
* @param slot P25 Phase 2 slot instance.
* @return bool True if ESS is complete, false otherwise.
*/
static bool p25P2ESSComplete(const p25::phase2::Slot& slot);
/**
* @brief Gets P25 Phase 2 ESS algorithm ID.
* @param slot P25 Phase 2 slot instance.
* @return uint8_t ESS algorithm ID of the slot.
*/
static uint8_t p25P2ESSAlgId(const p25::phase2::Slot& slot);
/**
* @brief Gets P25 Phase 2 ESS key ID.
* @param slot P25 Phase 2 slot instance.
* @return uint16_t ESS key ID of the slot.
*/
static uint16_t p25P2ESSKeyId(const p25::phase2::Slot& slot);
/**
* @brief Copies P25 Phase 2 ESS MI.
* @param slot P25 Phase 2 slot instance.
* @param mi Buffer to copy the ESS MI into.
*/
static void p25P2ESSMI(const p25::phase2::Slot& slot, uint8_t* mi);
/**
* @brief Gets the RF VCH lifecycle state.
* @param slot P25 Phase 2 slot instance.
* @return p25::phase2::Slot::VCH_STATE RF VCH lifecycle state of the slot.
*/
static p25::phase2::Slot::VCH_STATE p25P2RFVCHState(const p25::phase2::Slot& slot);
/**
* @brief Gets the network VCH lifecycle state.
* @param slot P25 Phase 2 slot instance.
* @return p25::phase2::Slot::VCH_STATE Network VCH lifecycle state of the slot.
*/
static p25::phase2::Slot::VCH_STATE p25P2NetVCHState(const p25::phase2::Slot& slot);
/**
* @brief Gets the decoded Phase 2 DUID.
* @param slot P25 Phase 2 slot instance.
* @return p25::defines::P2_DUID::E Decoded Phase 2 DUID of the slot.
*/
static p25::defines::P2_DUID::E p25P2DUID(const p25::phase2::Slot& slot);
/**
* @brief Gets the decoded inbound FACCH PTT count.
* @param slot P25 Phase 2 slot instance.
* @return uint8_t Decoded inbound FACCH PTT count of the slot.
*/
static uint8_t p25P2RFPTTCount(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of RF frames received.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of RF frames received by the slot.
*/
static uint32_t p25P2RFFrames(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of network frames received.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of network frames received by the slot.
*/
static uint32_t p25P2NetFrames(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of network frames lost.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of network frames lost by the slot.
*/
static uint32_t p25P2NetLost(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of network frames missed.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of network frames missed by the slot.
*/
static uint32_t p25P2NetMissed(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of RF bits received.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of RF bits received by the slot.
*/
static uint32_t p25P2RFBits(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of network bits received.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of network bits received by the slot.
*/
static uint32_t p25P2NetBits(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of RF errors.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of RF errors of the slot.
*/
static uint32_t p25P2RFErrs(const p25::phase2::Slot& slot);
/**
* @brief Gets the number of network errors.
* @param slot P25 Phase 2 slot instance.
* @return uint32_t Number of network errors of the slot.
*/
static uint32_t p25P2NetErrs(const p25::phase2::Slot& slot);
/**
* @brief Injects a synthetic P25 network call start via voice packet path.
* @param control P25 control instance.

@ -0,0 +1,88 @@
// 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 "common/p25/P25Defines.h"
#include "common/p25/lc/LC.h"
#include "common/p25/lc/mac/MACFactory.h"
#include "common/p25/lc/mac/MAC_GROUP_VCH_USER.h"
#include "common/p25/lc/mac/MAC_RELEASE.h"
#include "common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h"
#include "common/p25/lc/mac/MAC_UU_VCH_USER.h"
#include <catch2/catch_test_macros.hpp>
using namespace p25;
using namespace p25::lc;
using namespace p25::lc::mac;
TEST_CASE("P25 Phase 2 group MAC PDU carries LC fields", "[p2][mac]")
{
LC control;
control.setLCO(defines::P2_MAC_MCO::GROUP);
control.setSrcId(0x123456U);
control.setDstId(0x2345U);
control.setGroup(true);
control.setEmergency(true);
control.setEncrypted(true);
control.setPriority(5U);
MAC_GROUP_VCH_USER pdu;
REQUIRE(pdu.decode(control));
REQUIRE(pdu.getSrcId() == 0x123456U);
REQUIRE(pdu.getDstId() == 0x2345U);
REQUIRE(pdu.getGroup());
REQUIRE(pdu.getEmergency());
REQUIRE(pdu.getEncrypted());
REQUIRE(pdu.getPriority() == 5U);
LC encoded;
pdu.encode(encoded);
REQUIRE(encoded.getLCO() == defines::P2_MAC_MCO::GROUP);
REQUIRE(encoded.getSrcId() == 0x123456U);
REQUIRE(encoded.getDstId() == 0x2345U);
}
TEST_CASE("P25 Phase 2 private and release MAC PDUs validate opcodes", "[p2][mac]")
{
LC privateControl;
privateControl.setLCO(defines::P2_MAC_MCO::PRIVATE);
privateControl.setGroup(false);
privateControl.setSrcId(0x123456U);
privateControl.setDstId(0x654321U);
MAC_UU_VCH_USER privatePdu;
REQUIRE(privatePdu.decode(privateControl));
REQUIRE(privatePdu.getDstId() == 0x654321U);
LC releaseControl;
releaseControl.setLCO(defines::P2_MAC_MCO::MAC_RELEASE);
MAC_RELEASE release;
REQUIRE(release.decode(releaseControl));
REQUIRE_FALSE(privatePdu.decode(releaseControl));
LC telephoneControl;
telephoneControl.setLCO(defines::P2_MAC_MCO::TEL_INT_VCH_USER);
MAC_TEL_INT_VCH_USER telephone;
REQUIRE(telephone.decode(telephoneControl));
}
TEST_CASE("P25 Phase 2 MAC factory selects by opcode", "[p2][mac]")
{
LC control;
control.setLCO(defines::P2_MAC_MCO::GROUP);
control.setGroup(true);
std::unique_ptr<MACPDU> mac = MACFactory::createMACPDU(control);
REQUIRE(mac != nullptr);
REQUIRE(mac->getOpcode() == defines::P2_MAC_MCO::GROUP);
control.setLCO(0x7FU);
REQUIRE(MACFactory::createMACPDU(control) == nullptr);
}

@ -0,0 +1,802 @@
// 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/p25/phase2/Control.h"
#include "host/p25/phase2/Slot.h"
#include "host/HostTestHooks.h"
#include "common/BitManipulation.h"
#include "common/edac/RS634717.h"
#include "common/p25/P25Defines.h"
#include "common/p25/lc/mac/MACFactory.h"
#include "host/modem/Modem.h"
#include "host/modem/port/IModemPort.h"
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <cstring>
#include <vector>
using namespace p25::phase2;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
namespace {
/**
* @brief Creates an inbound MAC frame for testing purposes.
* @param duid The DUID of the MAC frame.
* @param opcode The MAC PDU opcode.
* @param srcId The source ID for the MAC frame.
* @param dstId The destination ID for the MAC frame.
* @return An array representing the inbound MAC frame.
*/
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> makeInboundMAC(
P25DEF::P2_DUID::E duid, uint8_t opcode, uint32_t srcId = 0x123456U,
uint32_t dstId = 0x2345U)
{
p25::lc::LC control;
control.setGroup(true);
control.setLCO(P25DEF::P2_MAC_MCO::GROUP);
control.setSrcId(srcId);
control.setDstId(dstId);
control.setP2DUID(static_cast<uint8_t>(duid));
control.setMACPDUOpcode(opcode);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> frame{};
frame[0U] = modem::TAG_DATA;
frame[1U] = static_cast<uint8_t>(duid);
control.encodeVCH_MACPDU_IEMI(frame.data() + 2U, duid == P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
return frame;
}
/**
* @brief Creates a P25 Phase 2 voice burst frame for testing purposes.
* @param duid The DUID of the voice burst.
* @param ess The encoded speech segment data.
* @param essBNo The block number within the ESS.
* @return An array representing the voice burst frame.
*/
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> makeVoiceBurst(
P25DEF::P2_DUID::E duid, const std::array<uint8_t, 44U>& ess, uint32_t essBNo)
{
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> frame{};
frame[0U] = modem::TAG_DATA;
frame[1U] = static_cast<uint8_t>(duid);
uint8_t* burst = frame.data() + 2U;
if (duid == P25DEF::P2_DUID::VTCH_4V) {
for (uint32_t bit = 0U; bit < 24U; ++bit)
WRITE_BIT(burst, 148U + bit, READ_BIT(ess.data(), essBNo * 24U + bit));
} else {
for (uint32_t bit = 0U; bit < 168U; ++bit)
WRITE_BIT(burst, 148U + bit, READ_BIT(ess.data(), 96U + bit));
}
return frame;
}
/**
* @brief Decodes the MAC PDU opcode from a queued P25 Phase 2 frame.
* @param frame The queued P25 Phase 2 frame.
* @return The decoded MAC PDU opcode.
*/
uint8_t decodeQueuedMACOpcode(const std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES>& frame)
{
p25::lc::LC decoded;
const auto duid = static_cast<P25DEF::P2_DUID::E>(frame[1U]);
const bool sync = duid == P25DEF::P2_DUID::FACCH_UNSCRAMBLED;
REQUIRE(decoded.decodeVCH_MACPDU_OEMI(frame.data() + 2U, sync));
return decoded.getMACPDUOpcode();
}
} // namespace
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief A mock implementation of the P25AffiliationLookup for testing grant behavior.
*/
class P2GrantLookup final : public p25::lookups::P25AffiliationLookup {
public:
/**
* @brief Initializes a new P2GrantLookup instance.
*/
P2GrantLookup() : P25AffiliationLookup(nullptr, nullptr, false), grantedDstId(0U),
touches(0U), releases(0U) { }
/**
* @brief Checks if a grant is currently active for the specified destination ID.
* @param dstId The destination ID to check.
* @return True if the grant is active, false otherwise.
*/
bool isGranted(uint32_t dstId) const override { return dstId != 0U && dstId == grantedDstId; }
/**
* @brief Marks the grant for the specified destination ID as touched.
* @param dstId The destination ID whose grant is being touched.
*/
void touchGrant(uint32_t dstId) override
{
if (isGranted(dstId))
++touches;
}
/**
* @brief Releases the grant for the specified destination ID.
* @param dstId The destination ID whose grant is being released.
* @param releaseAll Indicates whether all grants should be released.
* @return True if the grant was successfully released, false otherwise.
*/
bool releaseGrant(uint32_t dstId, bool releaseAll) override
{
(void)releaseAll;
if (!isGranted(dstId))
return false;
++releases;
grantedDstId = 0U;
return true;
}
uint32_t grantedDstId;
uint32_t touches;
uint32_t releases;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief A loopback port implementation for testing P25 Phase 2 modems.
*/
class P2LoopbackPort final : public modem::port::IModemPort {
public:
/**
* @brief Opens the loopback port.
* @return Always returns true.
*/
bool open() override { return true; }
/**
* @brief Reads data from the loopback port.
* @param buffer The buffer to store the read data.
* @param length The maximum number of bytes to read.
* @return Always returns 0, as no data is available.
*/
int read(uint8_t* buffer, uint32_t length) override
{
(void)buffer;
(void)length;
return 0;
}
/**
* @brief Writes data to the loopback port.
* @param buffer The buffer containing the data to write.
* @param length The number of bytes to write.
* @return The number of bytes written.
*/
int write(const uint8_t* buffer, uint32_t length) override
{
writes.emplace_back(buffer, buffer + length);
return static_cast<int>(length);
}
/**
* @brief Closes the loopback port.
*/
void close() override { }
std::vector<std::vector<uint8_t>> writes;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief A loopback modem implementation for testing P25 Phase 2 modems.
*/
class P2LoopbackModem final : public modem::Modem {
public:
/**
* @brief Constructs a loopback modem with the specified loopback port.
* @param port The loopback port to use with the modem.
*/
explicit P2LoopbackModem(P2LoopbackPort* port) :
modem::Modem(port, false, false, false, false, false, false, 0U, 0U, 0U,
1024U, 4096U, 1024U, true, true, false, false, false, false)
{
setModeParams(false, true, false);
m_p25P2Capable = true;
m_p25P2Space1 = 4096U;
m_p25P2Space2 = 4096U;
}
};
TEST_CASE("P25 Phase 2 control isolates slot queues", "[p25][p2][control]")
{
p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> slot0Frame{};
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> slot1Frame{};
slot0Frame[0U] = 0x10U;
slot1Frame[0U] = 0x20U;
REQUIRE(control.processFrame(0U, slot0Frame.data(), slot0Frame.size()));
REQUIRE(control.processFrame(1U, slot1Frame.data(), slot1Frame.size()));
REQUIRE(control.peekFrameLength(0U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES);
REQUIRE(control.peekFrameLength(1U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> output{};
REQUIRE(control.getFrame(0U, output.data()) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES);
auto slot0Output = output;
REQUIRE(slot0Output[0U] == modem::TAG_DATA);
REQUIRE(slot0Output[1U] == P25DEF::P2_DUID::VTCH_4V);
REQUIRE(control.getFrame(1U, output.data()) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES);
auto slot1Output = output;
REQUIRE(slot1Output[0U] == modem::TAG_DATA);
REQUIRE(slot1Output[1U] == P25DEF::P2_DUID::VTCH_4V);
REQUIRE(slot0Output.size() == slot1Output.size());
REQUIRE(control.getFrame(2U, output.data()) == 0U);
}
TEST_CASE("P25 Phase 2 RF processing forwards to network and RF", "[p25][p2][repeater]")
{
p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> frame{};
frame[0U] = 0x5AU;
REQUIRE(control.processFrame(1U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 1U)) == RS_RF_LATE_ENTRY);
REQUIRE(control.peekFrameLength(1U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES);
}
TEST_CASE("P25 Phase 2 rejects invalid frame lengths", "[p25][p2][validation]")
{
p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES - 1U> frame{};
REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE(control.peekFrameLength(0U) == 0U);
REQUIRE(control.isQueueFull(2U));
}
TEST_CASE("P25 Phase 2 slot returns to listening after RF timeout", "[p25][p2][lifecycle]")
{
p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> frame{};
REQUIRE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LATE_ENTRY);
control.clock(1001U);
REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LISTENING);
REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LISTENING);
}
TEST_CASE("P25 Phase 2 RF processing populates slot call state", "[p25][p2][rf][metadata]")
{
p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> frame{};
REQUIRE(control.processFrame(1U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2LastDstId(HostTestHooks::p25P2Slot(control, 1U)) == 0U);
REQUIRE(HostTestHooks::p25P2LastSrcId(HostTestHooks::p25P2Slot(control, 1U)) == 0U);
REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 1U)) == RS_RF_LATE_ENTRY);
}
TEST_CASE("P25 Phase 2 tracks burst scrambler offsets", "[p25][p2][scrambler]")
{
p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> frame{};
REQUIRE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == 0U);
REQUIRE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == P25DEF::P25_P2_BURST_LENGTH_BITS * 2U);
REQUIRE(control.processFrame(1U, frame.data(), frame.size()));
REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 1U)) == P25DEF::P25_P2_BURST_LENGTH_BITS);
control.reset();
REQUIRE(HostTestHooks::p25P2Slot(control, 0U).processNetwork(frame.data(), frame.size(), p25::lc::LC(),
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(HostTestHooks::p25P2NetScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == P25DEF::P25_P2_BURST_LENGTH_BITS / 2U);
}
TEST_CASE("P25 Phase 2 tagged modem frame decodes FACCH PTT before dispatch", "[p25][p2][vch]")
{
p25::phase2::Control control(true, 2U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
p25::lc::LC mac;
mac.setP2DUID(P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
mac.setMACPDUOpcode(P25DEF::P2_MAC_HEADER_OPCODE::PTT);
mac.setSrcId(0x123456U);
mac.setDstId(0x2345U);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> frame{};
frame[0U] = modem::TAG_DATA;
frame[1U] = P25DEF::P2_DUID::FACCH_UNSCRAMBLED;
mac.encodeVCH_MACPDU_IEMI(frame.data() + 2U, true);
REQUIRE(control.processFrame(0U, frame.data(), frame.size()));
const p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U);
REQUIRE(HostTestHooks::p25P2DUID(slot) == P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT);
REQUIRE(HostTestHooks::p25P2RFPTTCount(slot) == 1U);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> output{};
bool imm = false;
REQUIRE(control.getFrame(0U, output.data(), &imm) == output.size());
REQUIRE(imm);
REQUIRE(output[0U] == modem::TAG_DATA);
REQUIRE(output[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
p25::lc::LC decoded;
REQUIRE(decoded.decodeVCH_MACPDU_OEMI(output.data() + 2U, true));
REQUIRE(decoded.getMACPDUOpcode() == P25DEF::P2_MAC_HEADER_OPCODE::PTT);
}
TEST_CASE("P25 Phase 2 VCH processor rejects typed LCCH bursts", "[p25][p2][lcch]")
{
p25::phase2::Control control(true, 2U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr,
4096U, false, false);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> frame{};
frame[0U] = modem::TAG_DATA;
frame[1U] = P25DEF::P2_DUID::LCCH_UNSCRAMBLED;
REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size()));
REQUIRE(control.peekFrameLength(0U) == 0U);
}
TEST_CASE("P25 Phase 2 lost modem frame terminates the active slot", "[p25][p2][loss]")
{
p25::phase2::Control control(true, 0U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE(control.processFrame(1U, ptt.data(), ptt.size()));
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> voice{};
REQUIRE(control.processFrame(1U, voice.data(), voice.size()));
uint8_t lost = modem::TAG_LOST;
REQUIRE_FALSE(control.processFrame(1U, &lost, 1U));
control.clock(1501U);
REQUIRE(HostTestHooks::p25P2RFVCHState(HostTestHooks::p25P2Slot(control, 1U)) == p25::phase2::Slot::VCH_STATE::TERMINATING);
}
TEST_CASE("P25 Phase 2 public state management covers late entry collision and reset",
"[p25][p2][state]")
{
p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
p25::phase2::Slot& slot = control.slot(0U);
REQUIRE(control.getRFState(0U) == RS_RF_LISTENING);
REQUIRE(control.getNetState(0U) == RS_NET_IDLE);
REQUIRE(control.getRFState(2U) == RS_RF_INVALID);
REQUIRE_FALSE(control.isBusy());
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> voice{};
voice[0U] = modem::TAG_DATA;
voice[1U] = P25DEF::P2_DUID::VTCH_4V;
REQUIRE(control.processFrame(0U, voice.data(), voice.size()));
REQUIRE(control.getRFState(0U) == RS_RF_LATE_ENTRY);
REQUIRE(control.isBusy());
auto active = makeInboundMAC(P25DEF::P2_DUID::SACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE);
REQUIRE(control.processFrame(0U, active.data(), active.size()));
REQUIRE(control.getRFState(0U) == RS_RF_AUDIO);
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE);
control.reset();
REQUIRE(control.getRFState(0U) == RS_RF_LISTENING);
REQUIRE(control.getNetState(0U) == RS_NET_IDLE);
REQUIRE_FALSE(control.isBusy());
p25::lc::LC call;
call.setSrcId(0x123456U);
call.setDstId(0x2345U);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> networkVoice{};
REQUIRE(slot.processNetwork(networkVoice.data(), networkVoice.size(), call,
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(control.getNetState(0U) == RS_NET_AUDIO);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE_FALSE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE(control.getRFState(0U) == RS_RF_LISTENING);
REQUIRE(control.getNetState(0U) == RS_NET_AUDIO);
control.clock(1501U);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> queued{};
while (control.getFrame(0U, queued.data()) != 0U) { }
REQUIRE(control.getNetState(0U) == RS_NET_IDLE);
REQUIRE(control.getRFState(0U) == RS_RF_LISTENING);
REQUIRE_FALSE(control.isBusy());
}
TEST_CASE("P25 Phase 2 non-authoritative RF state requires a permitted destination",
"[p25][p2][state][authoritative]")
{
p25::phase2::Control control(false, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE_FALSE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE(control.getRFState(0U) == RS_RF_LISTENING);
REQUIRE_FALSE(control.isBusy());
control.slot(0U).permittedTG(0x2345U);
REQUIRE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE(control.getRFState(0U) == RS_RF_AUDIO);
REQUIRE(control.isBusy());
}
TEST_CASE("P25 Phase 2 rejected RF state is explicitly recoverable",
"[p25][p2][state][rejection]")
{
p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
p25::phase2::Slot& slot = control.slot(1U);
HostTestHooks::p25P2SetRFState(slot, RS_RF_REJECTED);
REQUIRE(control.getRFState(1U) == RS_RF_REJECTED);
REQUIRE(control.isBusy());
control.clearRFReject(1U);
REQUIRE(control.getRFState(1U) == RS_RF_LISTENING);
REQUIRE_FALSE(control.isBusy());
}
TEST_CASE("P25 Phase 2 network timeout is clocked once and honors authorization",
"[p25][p2][state][network][timeout]")
{
p25::phase2::Control control(false, 0U, 2U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
p25::phase2::Slot& slot = control.slot(0U);
p25::lc::LC call;
call.setSrcId(0x123456U);
call.setDstId(0x2345U);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> voice{};
REQUIRE_FALSE(slot.processNetwork(voice.data(), voice.size(), call,
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(control.getNetState(0U) == RS_NET_IDLE);
slot.permittedTG(0x2345U);
REQUIRE(slot.processNetwork(voice.data(), voice.size(), call,
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(control.getNetState(0U) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25P2NetVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE);
control.clock(1001U);
REQUIRE(control.getNetState(0U) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25P2NetVCHState(slot) != p25::phase2::Slot::VCH_STATE::TERMINATING);
}
TEST_CASE("P25 Phase 2 permits are slot specific", "[p25][p2][permit][authoritative]")
{
p25::phase2::Control control(false, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
control.permittedTG(0x2345U, 0U);
REQUIRE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE_FALSE(control.processFrame(1U, ptt.data(), ptt.size()));
REQUIRE(control.getRFState(0U) == RS_RF_AUDIO);
REQUIRE(control.getRFState(1U) == RS_RF_LISTENING);
}
TEST_CASE("P25 Phase 2 accepted traffic touches and releases its CC grant",
"[p25][p2][grant]")
{
P2GrantLookup grants;
grants.grantedDstId = 0x2345U;
p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, &grants, nullptr,
nullptr, 4096U, false, false);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE(grants.touches == 1U);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> voice{};
voice[0U] = modem::TAG_DATA;
voice[1U] = P25DEF::P2_DUID::VTCH_4V;
REQUIRE(control.processFrame(0U, voice.data(), voice.size()));
REQUIRE(grants.touches == 2U);
control.touchGrantTG(0x2345U, 0U);
REQUIRE(grants.touches == 3U);
control.releaseGrantTG(0x2345U, 0U);
REQUIRE(grants.releases == 1U);
REQUIRE(grants.grantedDstId == 0U);
}
TEST_CASE("P25 Phase 2 RF call establishment enforces RID and TGID ACLs",
"[p25][p2][acl]")
{
::lookups::RadioIdLookup ridLookup("", 0U, true, false);
::lookups::TalkgroupRulesLookup tidLookup("", 0U, true, false);
ridLookup.addEntry(0x123456U, true, "allowed source");
tidLookup.addEntry(0x2345U, 0U, true);
p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, &ridLookup,
&tidLookup, 4096U, false, false);
auto groupPTT = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE(control.processFrame(0U, groupPTT.data(), groupPTT.size()));
REQUIRE(control.getRFState(0U) == RS_RF_AUDIO);
control.reset();
auto deniedSource = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT, 0x654321U, 0x2345U);
REQUIRE_FALSE(control.processFrame(0U, deniedSource.data(), deniedSource.size()));
REQUIRE(control.getRFState(0U) == RS_RF_REJECTED);
control.clearRFReject(0U);
auto deniedTG = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT, 0x123456U, 0x3456U);
REQUIRE_FALSE(control.processFrame(0U, deniedTG.data(), deniedTG.size()));
REQUIRE(control.getRFState(0U) == RS_RF_REJECTED);
control.clearRFReject(0U);
p25::lc::LC privateCall;
privateCall.setGroup(false);
privateCall.setMACPartition(P25DEF::P2_MAC_MCO_PARTITION::UNIQUE);
privateCall.setLCO(P25DEF::P2_MAC_MCO::PRIVATE);
privateCall.setSrcId(0x123456U);
privateCall.setDstId(0x456789U);
privateCall.setP2DUID(P25DEF::P2_DUID::SACCH_UNSCRAMBLED);
privateCall.setMACPDUOpcode(P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> privatePTT{};
privatePTT[0U] = modem::TAG_DATA;
privatePTT[1U] = P25DEF::P2_DUID::SACCH_UNSCRAMBLED;
privateCall.encodeVCH_MACPDU_IEMI(privatePTT.data() + 2U, false);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> lateVoice{};
lateVoice[0U] = modem::TAG_DATA;
lateVoice[1U] = P25DEF::P2_DUID::VTCH_4V;
REQUIRE(control.processFrame(0U, lateVoice.data(), lateVoice.size()));
REQUIRE_FALSE(control.processFrame(0U, privatePTT.data(), privatePTT.size()));
REQUIRE(control.getRFState(0U) == RS_RF_REJECTED);
control.clearRFReject(0U);
ridLookup.addEntry(0x456789U, true, "allowed target");
REQUIRE(control.processFrame(0U, lateVoice.data(), lateVoice.size()));
REQUIRE(control.processFrame(0U, privatePTT.data(), privatePTT.size()));
REQUIRE(control.getRFState(0U) == RS_RF_AUDIO);
}
TEST_CASE("P25 Phase 2 host modem logical loopback preserves both slots", "[p25][p2][e2e][modem]")
{
p25::phase2::Control transmitter(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr,
nullptr, nullptr, 4096U, false, false);
p25::phase2::Control receiver(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr,
nullptr, nullptr, 4096U, false, false);
auto* port = new P2LoopbackPort();
P2LoopbackModem airModem(port);
for (uint32_t slotNo = 0U; slotNo < p25::phase2::Control::SLOT_COUNT; ++slotNo) {
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> input{};
input[0U] = modem::TAG_DATA;
input[1U] = P25DEF::P2_DUID::VTCH_4V;
input[2U] = static_cast<uint8_t>(0x20U + slotNo);
REQUIRE(transmitter.processFrame(slotNo, input.data(), input.size()));
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> hostFrame{};
REQUIRE(transmitter.getFrame(slotNo, hostFrame.data()) == hostFrame.size());
REQUIRE(hostFrame[0U] == modem::TAG_DATA);
REQUIRE(hostFrame[1U] == P25DEF::P2_DUID::VTCH_4V);
const bool written = slotNo == 0U ?
airModem.writeP25P2Frame1(hostFrame.data(), hostFrame.size()) :
airModem.writeP25P2Frame2(hostFrame.data(), hostFrame.size());
REQUIRE(written);
REQUIRE(port->writes.size() == slotNo + 1U);
const std::vector<uint8_t>& serialFrame = port->writes.back();
REQUIRE(serialFrame.size() == P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U);
REQUIRE(serialFrame[0U] == modem::DVM_SHORT_FRAME_START);
REQUIRE(serialFrame[1U] == serialFrame.size());
REQUIRE(serialFrame[2U] == (slotNo == 0U ? modem::CMD_P25_P2_DATA1 : modem::CMD_P25_P2_DATA2));
REQUIRE(serialFrame[3U] == P25DEF::P2_DUID::VTCH_4V);
REQUIRE(std::memcmp(serialFrame.data() + 4U, hostFrame.data() + 2U,
P25DEF::P25_P2_FRAME_LENGTH_BYTES) == 0);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> received{};
received[0U] = modem::TAG_DATA;
std::memcpy(received.data() + 1U, serialFrame.data() + 3U, serialFrame.size() - 3U);
REQUIRE(receiver.processFrame(slotNo, received.data(), received.size()));
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> repeated{};
REQUIRE(receiver.getFrame(slotNo, repeated.data()) == repeated.size());
REQUIRE(repeated == hostFrame);
REQUIRE(receiver.peekFrameLength(1U - slotNo) == 0U);
}
}
TEST_CASE("P25 Phase 2 deterministic full RF call lifecycle", "[p25][p2][e2e][call]")
{
p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 8192U, false, false);
p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U);
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> queued{};
bool immediate = false;
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE(control.processFrame(0U, ptt.data(), ptt.size()));
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT);
REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size());
REQUIRE(immediate);
REQUIRE(queued[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::PTT);
std::array<uint8_t, 44U> ess{};
ess[0U] = 0x80U;
ess[1U] = 0x12U;
ess[2U] = 0x34U;
for (uint32_t i = 0U; i < 9U; ++i)
ess[3U + i] = static_cast<uint8_t>(0xA0U + i);
edac::RS634717().encode441629(ess.data());
auto active = makeInboundMAC(P25DEF::P2_DUID::SACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE);
p25::lc::LC decodedActive;
REQUIRE(decodedActive.decodeVCH_MACPDU_IEMI(active.data() + 2U, false));
REQUIRE(decodedActive.getP2DUID() == P25DEF::P2_DUID::SACCH_UNSCRAMBLED);
REQUIRE(decodedActive.getMACPDUOpcode() == P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE);
REQUIRE(decodedActive.getLCO() == P25DEF::P2_MAC_MCO::GROUP);
REQUIRE(p25::lc::mac::MACFactory::createMACPDU(decodedActive) != nullptr);
for (uint32_t essBNo = 0U; essBNo < 4U; ++essBNo) {
auto voice4V = makeVoiceBurst(P25DEF::P2_DUID::VTCH_4V, ess, essBNo);
REQUIRE(control.processFrame(0U, voice4V.data(), voice4V.size()));
REQUIRE(control.processFrame(0U, active.data(), active.size()));
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE);
auto voice2V = makeVoiceBurst(P25DEF::P2_DUID::VTCH_2V, ess, essBNo);
REQUIRE(control.processFrame(0U, voice2V.data(), voice2V.size()));
}
REQUIRE(HostTestHooks::p25P2ESSComplete(slot));
REQUIRE(HostTestHooks::p25P2ESSAlgId(slot) == 0x80U);
REQUIRE(HostTestHooks::p25P2ESSKeyId(slot) == 0x1234U);
REQUIRE(HostTestHooks::p25P2RFFrames(slot) == 8U);
REQUIRE(HostTestHooks::p25P2RFBits(slot) == 1729U);
REQUIRE(HostTestHooks::p25P2RFErrs(slot) > 0U);
REQUIRE(HostTestHooks::p25P2RFErrs(slot) <= HostTestHooks::p25P2RFBits(slot));
auto endPTT = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::END_PTT);
REQUIRE(control.processFrame(0U, endPTT.data(), endPTT.size()));
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE);
REQUIRE(control.processFrame(0U, endPTT.data(), endPTT.size()));
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::HANGTIME);
REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size());
REQUIRE(immediate);
REQUIRE(queued[1U] == P25DEF::P2_DUID::SACCH_UNSCRAMBLED);
REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::HANGTIME);
control.clock(360U);
control.clock(641U);
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::TERMINATING);
for (uint32_t i = 0U; i < 2U; ++i) {
REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size());
REQUIRE(immediate);
REQUIRE(queued[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::END_PTT);
}
bool saw4V = false;
bool saw2V = false;
uint32_t sacchCount = 0U;
while (control.getFrame(0U, queued.data(), &immediate) != 0U) {
REQUIRE_FALSE(immediate);
saw4V |= queued[1U] == P25DEF::P2_DUID::VTCH_4V;
saw2V |= queued[1U] == P25DEF::P2_DUID::VTCH_2V;
if (queued[1U] == P25DEF::P2_DUID::SACCH_UNSCRAMBLED)
++sacchCount;
}
REQUIRE(saw4V);
REQUIRE(saw2V);
// Four inbound ACTIVE SACCH bursts are repeated, and the 1,001 ms
// hangtime interval inserts two additional 360 ms cadence SACCH bursts.
REQUIRE(sacchCount == 6U);
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE);
REQUIRE(HostTestHooks::p25P2RFState(slot) == RS_RF_LISTENING);
REQUIRE(HostTestHooks::p25P2RFFrames(slot) == 0U);
REQUIRE(HostTestHooks::p25P2RFBits(slot) == 1U);
REQUIRE(HostTestHooks::p25P2RFErrs(slot) == 0U);
}
TEST_CASE("P25 Phase 2 slot accounts network frames loss misses and BER",
"[p25][p2][statistics]")
{
p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U);
p25::lc::LC call;
call.setSrcId(0x123456U);
call.setDstId(0x2345U);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> voice4V{};
voice4V[0U] = 0x20U; // one error in the first outbound AMBE+2 codeword
REQUIRE(slot.processNetwork(voice4V.data(), voice4V.size(), call,
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 1U);
REQUIRE(HostTestHooks::p25P2NetBits(slot) == 289U);
REQUIRE(HostTestHooks::p25P2NetErrs(slot) > 0U);
std::array<uint8_t, P25DEF::P25_P2_FRAME_LENGTH_BYTES> voice2V{};
REQUIRE(slot.processNetwork(voice2V.data(), voice2V.size(), call,
P25DEF::P2_DUID::VTCH_2V, 0U));
REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 2U);
REQUIRE(HostTestHooks::p25P2NetBits(slot) == 433U);
p25::lc::LC competing(call);
competing.setDstId(0x3456U);
REQUIRE_FALSE(slot.processNetwork(voice4V.data(), voice4V.size(), competing,
P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(HostTestHooks::p25P2NetMissed(slot) == 1U);
control.clock(1501U);
REQUIRE(HostTestHooks::p25P2NetLost(slot) == 1U);
slot.reset();
REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 0U);
REQUIRE(HostTestHooks::p25P2NetLost(slot) == 0U);
REQUIRE(HostTestHooks::p25P2NetMissed(slot) == 0U);
REQUIRE(HostTestHooks::p25P2NetBits(slot) == 1U);
REQUIRE(HostTestHooks::p25P2NetErrs(slot) == 0U);
}
TEST_CASE("P25 Phase 2 deterministic call recovers from missing termination",
"[p25][p2][e2e][call][loss]")
{
p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr,
nullptr, 4096U, false, false);
p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 1U);
auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED,
P25DEF::P2_MAC_HEADER_OPCODE::PTT);
REQUIRE(control.processFrame(1U, ptt.data(), ptt.size()));
std::array<uint8_t, P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES> queued{};
bool immediate = false;
REQUIRE(control.getFrame(1U, queued.data(), &immediate) == queued.size());
REQUIRE(immediate);
REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::PTT);
std::array<uint8_t, 44U> emptyESS{};
auto voice = makeVoiceBurst(P25DEF::P2_DUID::VTCH_4V, emptyESS, 0U);
REQUIRE(control.processFrame(1U, voice.data(), voice.size()));
uint8_t lost = modem::TAG_LOST;
REQUIRE_FALSE(control.processFrame(1U, &lost, 1U));
control.clock(1501U);
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::TERMINATING);
for (uint32_t i = 0U; i < 2U; ++i) {
REQUIRE(control.getFrame(1U, queued.data(), &immediate) == queued.size());
REQUIRE(immediate);
REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::END_PTT);
}
while (control.getFrame(1U, queued.data(), &immediate) != 0U) { }
REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE);
REQUIRE(HostTestHooks::p25P2RFState(slot) == RS_RF_LISTENING);
}

@ -0,0 +1,63 @@
// 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/p25/phase2/Slot.h"
#include "host/HostTestHooks.h"
#include "common/BitManipulation.h"
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <cstring>
using namespace p25::phase2;
TEST_CASE("P25 Phase 2 ESS reconstructs ALGID KID and MI", "[p2][ess]")
{
edac::RS634717 rs;
std::array<uint8_t, 44U> codeword{};
codeword[0U] = 0x80U;
codeword[1U] = 0x12U;
codeword[2U] = 0x34U;
for (uint32_t i = 0U; i < 9U; i++) codeword[3U + i] = static_cast<uint8_t>(0xA0U + i);
rs.encode441629(codeword.data());
Slot::init(nullptr, true, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr);
Slot slot(0U, 1000U, 1000U, 4096U, false, false);
p25::lc::LC mac;
mac.setGroup(true);
mac.setLCO(P25DEF::P2_MAC_MCO::GROUP);
mac.setP2DUID(P25DEF::P2_DUID::FACCH_UNSCRAMBLED);
mac.setMACPDUOpcode(P25DEF::P2_MAC_HEADER_OPCODE::PTT);
std::array<uint8_t, 40U> macBurst{};
mac.encodeVCH_MACPDU(macBurst.data(), true);
for (uint32_t burstNo = 0U; burstNo < 4U; burstNo++) {
std::array<uint8_t, 40U> burst{};
for (uint32_t bit = 0U; bit < 24U; bit++)
WRITE_BIT(burst.data(), 148U + bit, READ_BIT(codeword.data(), burstNo * 24U + bit));
REQUIRE(slot.processNetwork(burst.data(), burst.size(), p25::lc::LC(), P25DEF::P2_DUID::VTCH_4V, 0U));
REQUIRE(slot.processNetwork(macBurst.data(), macBurst.size(), mac,
P25DEF::P2_DUID::FACCH_UNSCRAMBLED, 0U));
}
std::array<uint8_t, 40U> burst2V{};
for (uint32_t bit = 0U; bit < 168U; bit++)
WRITE_BIT(burst2V.data(), 148U + bit, READ_BIT(codeword.data(), 96U + bit));
REQUIRE(slot.processNetwork(burst2V.data(), burst2V.size(), p25::lc::LC(), P25DEF::P2_DUID::VTCH_2V, 0U));
REQUIRE(HostTestHooks::p25P2ESSComplete(slot));
REQUIRE(HostTestHooks::p25P2ESSAlgId(slot) == 0x80U);
REQUIRE(HostTestHooks::p25P2ESSKeyId(slot) == 0x1234U);
uint8_t mi[9U] = {0U};
HostTestHooks::p25P2ESSMI(slot, mi);
REQUIRE(std::memcmp(mi, codeword.data() + 3U, sizeof(mi)) == 0);
}

@ -24,19 +24,31 @@ using namespace p25::lc;
#include <stdlib.h>
#include <time.h>
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
namespace {
void configureP2ScrambleContext(LC& lc, uint16_t scrambleOffset, uint16_t colorCode = DEFAULT_NAC)
{
static bool siteConfigured = false;
if (!siteConfigured) {
LC::setSiteData(SiteData(0xABCDEU, 0x123U, 1U, 1U, 1U, 1U, 1U,
ServiceClass::VOICE | ServiceClass::DATA, 0));
siteConfigured = true;
}
lc.setColorCode(colorCode);
lc.setP2ScrambleOffset(scrambleOffset);
/**
* @brief Configures the LC instance with a P25 Phase 2 scramble context.
* @param lc LC instance to configure.
* @param scrambleOffset Scramble offset to set.
* @param colorCode Color code to set (default is DEFAULT_NAC).
* @note This function sets the site data for the LC instance if it has not been
*/
void configureP2ScrambleContext(LC& lc, uint16_t scrambleOffset, uint16_t colorCode = DEFAULT_NAC)
{
static bool siteConfigured = false;
if (!siteConfigured) {
LC::setSiteData(SiteData(0xABCDEU, 0x123U, 1U, 1U, 1U, 1U, 1U,
ServiceClass::VOICE | ServiceClass::DATA, 0));
siteConfigured = true;
}
lc.setColorCode(colorCode);
lc.setP2ScrambleOffset(scrambleOffset);
}
}
TEST_CASE("P25 Phase 2 VCH MAC PDU I-OEMI (RS 52,30,23) Test", "[p25][p2_vch_macpdu_ioemi][cap]") {

Loading…
Cancel
Save

Powered by TurnKey Linux.