baseline implementation of host-only P25 Phase 2 implementation (this creates the plumbing required later once the SDR modems have support for Phase 2);
parent
56bff0489b
commit
e9dc058704
@ -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__
|
||||
@ -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__
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
Loading…
Reference in new issue