From e9dc058704fd153856d37bc0521265cae945a3be Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Thu, 13 Aug 2026 23:08:53 -0400 Subject: [PATCH] baseline implementation of host-only P25 Phase 2 implementation (this creates the plumbing required later once the SDR modems have support for Phase 2); --- src/common/CMakeLists.txt | 2 + src/common/edac/AMBE2FEC.cpp | 194 +++ src/common/edac/AMBE2FEC.h | 161 +++ src/common/network/BaseNetwork.cpp | 2 +- src/common/p25/P25Defines.h | 19 +- src/common/p25/lc/LC.cpp | 135 +- src/common/p25/lc/LC.h | 9 +- src/common/p25/lc/MACPDU.cpp | 68 + src/common/p25/lc/MACPDU.h | 105 ++ src/common/p25/lc/mac/MACFactory.cpp | 54 + src/common/p25/lc/mac/MACFactory.h | 71 + src/common/p25/lc/mac/MAC_GROUP_VCH_USER.cpp | 39 + src/common/p25/lc/mac/MAC_GROUP_VCH_USER.h | 58 + src/common/p25/lc/mac/MAC_RELEASE.cpp | 32 + src/common/p25/lc/mac/MAC_RELEASE.h | 51 + .../p25/lc/mac/MAC_TEL_INT_VCH_USER.cpp | 39 + src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h | 58 + src/common/p25/lc/mac/MAC_UU_VCH_USER.cpp | 39 + src/common/p25/lc/mac/MAC_UU_VCH_USER.h | 58 + src/host/CMakeLists.txt | 4 + src/host/modem/Modem.cpp | 486 +++++++ src/host/modem/Modem.h | 95 +- src/host/p25/packet/phase2/Voice.cpp | 265 ++++ src/host/p25/packet/phase2/Voice.h | 172 +++ src/host/p25/phase2/Control.cpp | 294 ++++ src/host/p25/phase2/Control.h | 217 +++ src/host/p25/phase2/Slot.cpp | 1230 +++++++++++++++++ src/host/p25/phase2/Slot.h | 372 +++++ tests/edac/AMBE2FEC_Tests.cpp | 139 ++ tests/edac/AMBEFEC_Tests.cpp | 42 + tests/host/HostTestHooks.cpp | 114 ++ tests/host/HostTestHooks.h | 143 ++ tests/p25/MACPDU_Tests.cpp | 88 ++ tests/p25/P2_Control_Tests.cpp | 802 +++++++++++ tests/p25/P2_ESS_Tests.cpp | 63 + tests/p25/P2_VCH_MACPDU_Test.cpp | 32 +- 36 files changed, 5684 insertions(+), 68 deletions(-) create mode 100644 src/common/edac/AMBE2FEC.cpp create mode 100644 src/common/edac/AMBE2FEC.h create mode 100644 src/common/p25/lc/MACPDU.cpp create mode 100644 src/common/p25/lc/MACPDU.h create mode 100644 src/common/p25/lc/mac/MACFactory.cpp create mode 100644 src/common/p25/lc/mac/MACFactory.h create mode 100644 src/common/p25/lc/mac/MAC_GROUP_VCH_USER.cpp create mode 100644 src/common/p25/lc/mac/MAC_GROUP_VCH_USER.h create mode 100644 src/common/p25/lc/mac/MAC_RELEASE.cpp create mode 100644 src/common/p25/lc/mac/MAC_RELEASE.h create mode 100644 src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.cpp create mode 100644 src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h create mode 100644 src/common/p25/lc/mac/MAC_UU_VCH_USER.cpp create mode 100644 src/common/p25/lc/mac/MAC_UU_VCH_USER.h create mode 100644 src/host/p25/packet/phase2/Voice.cpp create mode 100644 src/host/p25/packet/phase2/Voice.h create mode 100644 src/host/p25/phase2/Control.cpp create mode 100644 src/host/p25/phase2/Control.h create mode 100644 src/host/p25/phase2/Slot.cpp create mode 100644 src/host/p25/phase2/Slot.h create mode 100644 tests/edac/AMBE2FEC_Tests.cpp create mode 100644 tests/edac/AMBEFEC_Tests.cpp create mode 100644 tests/p25/MACPDU_Tests.cpp create mode 100644 tests/p25/P2_Control_Tests.cpp create mode 100644 tests/p25/P2_ESS_Tests.cpp diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index df0da19f..a103d19e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -24,6 +24,7 @@ file(GLOB common_SRC "src/common/p25/dfsi/frames/*.cpp" "src/common/p25/dfsi/frames/fsc/*.cpp" "src/common/p25/lc/*.cpp" + "src/common/p25/lc/mac/*.cpp" "src/common/p25/lc/tdulc/*.cpp" "src/common/p25/lc/tsbk/*.cpp" "src/common/p25/lc/tsbk/mbt/*.cpp" @@ -76,6 +77,7 @@ file(GLOB common_INCLUDE "src/common/p25/dfsi/frames/*.h" "src/common/p25/dfsi/frames/fsc/*.h" "src/common/p25/lc/*.h" + "src/common/p25/lc/mac/*.h" "src/common/p25/lc/tdulc/*.h" "src/common/p25/lc/tsbk/*.h" "src/common/p25/lc/tsbk/mbt/*.h" diff --git a/src/common/edac/AMBE2FEC.cpp b/src/common/edac/AMBE2FEC.cpp new file mode 100644 index 00000000..f90f3bd8 --- /dev/null +++ b/src/common/edac/AMBE2FEC.cpp @@ -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 +#include + +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((dibit >> 1U) & 1U)); + writeWordBit(codeword, ref0, static_cast(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((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(1U << (7U - (offset & 7U))); + if (bit != 0U) + codeword[offset >> 3U] |= mask; + else + codeword[offset >> 3U] &= static_cast(~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(1U << (7U - ((offset + i) & 7U))); + bytes[(offset + i) >> 3U] = bit != 0U ? bytes[(offset + i) >> 3U] | mask : + bytes[(offset + i) >> 3U] & static_cast(~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; +} diff --git a/src/common/edac/AMBE2FEC.h b/src/common/edac/AMBE2FEC.h new file mode 100644 index 00000000..7074e32e --- /dev/null +++ b/src/common/edac/AMBE2FEC.h @@ -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__ diff --git a/src/common/network/BaseNetwork.cpp b/src/common/network/BaseNetwork.cpp index 4ee22542..6331a029 100644 --- a/src/common/network/BaseNetwork.cpp +++ b/src/common/network/BaseNetwork.cpp @@ -1676,7 +1676,7 @@ UInt8Array BaseNetwork::createP25P2_Message(uint32_t& length, const p25::lc::LC& buffer[14U] = controlByte; - buffer[19U] = slot ? 0x00U : 0x80U; // Slot Number + buffer[19U] = slot ? 0x80U : 0x00U; // Slot Number buffer[19U] |= (uint8_t)duid; // Phase 2 DUID // pack raw P25 Phase 2 bytes diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index 4a5277a2..8a62a7c0 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -70,6 +70,11 @@ namespace p25 // TIA-102.BBAC-A Section 4 const uint32_t P25_P2_FRAME_LENGTH_BYTES = 40U; const uint32_t P25_P2_FRAME_LENGTH_BITS = P25_P2_FRAME_LENGTH_BYTES * 8U; + // Host/modem frames carry a modem tag and explicit DUID followed by the + // 40-byte logical burst. The DUID is transport metadata and is not aired. + const uint32_t P25_P2_HOST_FRAME_LENGTH_BYTES = P25_P2_FRAME_LENGTH_BYTES + 2U; + const uint32_t P25_P2_BURST_LENGTH_BITS = 360U; + const uint32_t P25_P2_SCRAMBLER_SEQUENCE_BITS = 4320U; const uint32_t P25_P2_IEMI_LENGTH_BITS = 312U; const uint32_t P25_P2_IEMI_LENGTH_BYTES = (P25_P2_IEMI_LENGTH_BITS / 8U) + 1U; @@ -184,6 +189,10 @@ namespace p25 const uint32_t DEFAULT_SILENCE_THRESHOLD = 124U; /** @brief Default Frame Loss Threshold */ const uint32_t DEFAULT_FRAME_LOSS_THRESHOLD = 6U; + /** @brief Default Phase 2 Silence Threshold */ + const uint32_t P2_DEFAULT_SILENCE_THRESHOLD = 21U; + /** @brief Default Phase 2 Frame Loss Threshold */ + const uint32_t P2_DEFAULT_FRAME_LOSS_THRESHOLD = 2U; /** @brief Maximum P25 voice frame errors */ const uint32_t MAX_P25_VOICE_ERRORS = 1233U; /** @} */ @@ -864,7 +873,7 @@ namespace p25 }; } - // TIA-102.BBAD-D Section 4.1 + // TIA-102.BBAD-A Section 2.3.3 /** @brief Phase 2 MAC PDU Opcode(s) */ namespace P2_MAC_HEADER_OPCODE { /** @brief Phase 2 MAC PDU Opcode(s) */ @@ -879,7 +888,7 @@ namespace p25 }; } - // TIA-102.BBAD-D Section 4.2 + // TIA-102.BBAD-A Section 2.3.3 /** @brief Phase 2 MAC 4V/SACCH Offset(s) */ namespace P2_MAC_HEADER_OFFSET { /** @brief Phase 2 MAC 4V/SACCH Offset(s) */ @@ -897,7 +906,7 @@ namespace p25 }; } - // TIA-102.BBAD-D Section 3 + // TIA-102.BBAD-A Section 3 /** @brief Phase 2 MAC MCO Partitioning */ namespace P2_MAC_MCO_PARTITION { /** @brief Phase 2 MAC MCO Partitioning */ @@ -910,7 +919,7 @@ namespace p25 }; } - // TIA-102.BBAD-D Section 3 + // TIA-102.BBAD-A Section 3 /** @brief Phase 2 MAC PDU Opcode(s) */ namespace P2_MAC_MCO { /** @brief Phase 2 MAC PDU Opcode(s) */ @@ -952,9 +961,11 @@ namespace p25 enum E : uint8_t { VTCH_4V = 0x00U, //!< Inbound/Outbound 4V SACCH_SCRAMBLED = 0x03U, //!< SACCH Scrambled + LCCH_SCRAMBLED = 0x04U, //!< Inbound LCCH Scrambled VTCH_2V = 0x06U, //!< Inbound/Outbound 2V FACCH_SCRAMBLED = 0x09U, //!< FACCH Scrambled SACCH_UNSCRAMBLED = 0x0CU, //!< SACCH Unscrambled + LCCH_UNSCRAMBLED = 0x0DU, //!< Inbound/Outbound LCCH Unscrambled FACCH_UNSCRAMBLED = 0x0FU //!< FACCH Unscrambled }; } diff --git a/src/common/p25/lc/LC.cpp b/src/common/p25/lc/LC.cpp index 29241859..64ca20a1 100644 --- a/src/common/p25/lc/LC.cpp +++ b/src/common/p25/lc/LC.cpp @@ -198,7 +198,8 @@ LC::LC() : m_mi(nullptr), m_userAlias(nullptr), m_gotUserAliasPartA(false), - m_gotUserAlias(false) + m_gotUserAlias(false), + m_p2MCODataLength(0U) { m_mi = new uint8_t[MI_LENGTH_BYTES]; ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); @@ -655,7 +656,7 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync) m_p2DUID = duid[0U] >> 4U; - if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V || !sync) + if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V) return true; // don't handle 4V or 2V voice PDUs here -- user code will handle else { ::memset(raw, 0x00U, lengthBytes); @@ -669,14 +670,17 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync) source = burst; } - // IEMI with sync: extract data bits (skip 14-bit sync and DUIDs) + // Extract the four information fields, skipping sync (when present) and DUIDs. for (uint32_t i = 0U; i < lengthBits; i++) { - uint32_t n = i + 14U; // Skip 14-bit sync - if (i >= 36U) + uint32_t n = i + (sync ? 14U : 0U); + uint32_t field1 = sync ? 36U : 72U; + uint32_t field2 = field1 + 72U; + uint32_t field3 = field2 + 96U; + if (i >= field1) n += 2U; // skip DUID 1 after field 1 (36 bits) - if (i >= 108U) + if (i >= field2) n += 2U; // skip DUID 2 after field 2 (36+72) - if (i >= 204U) + if (i >= field3) n += 2U; // skip DUID 3 after field 3 (36+72+96) bool b = READ_BIT(source, n); @@ -712,6 +716,45 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync) return true; } +/* Encode an inbound/IEMI VCH MAC PDU. */ + +void LC::encodeVCH_MACPDU_IEMI(uint8_t* data, bool sync) +{ + assert(data != nullptr); + ::memset(data, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + + uint32_t lengthBits = sync ? P25_P2_IEMI_WSYNC_LENGTH_BITS : P25_P2_IEMI_LENGTH_BITS; + uint8_t raw[P25_P2_IEMI_LENGTH_BYTES] = { 0U }; + if (m_p2DUID != P2_DUID::VTCH_4V && m_p2DUID != P2_DUID::VTCH_2V) { + encodeMACPDU(raw, P25_P2_IEMI_MAC_LENGTH_BITS); + m_rs.encode462621(raw); + for (uint32_t i = 0U; i < lengthBits; i++) { + uint32_t field1 = sync ? 36U : 72U; + uint32_t field2 = field1 + 72U; + uint32_t field3 = field2 + 96U; + uint32_t n = i + (sync ? 14U : 0U); + if (i >= field1) n += 2U; + if (i >= field2) n += 2U; + if (i >= field3) n += 2U; + WRITE_BIT(data, n, READ_BIT(raw, i)); + } + } + + uint8_t duidRaw[2U] = { static_cast((m_p2DUID & 0x0FU) << 4U), 0U }; + uint8_t duid[2U] = { 0U }; + encodeP2_DUIDHamming(duid, duidRaw); + for (uint8_t i = 0U; i < 8U; i++) { + uint32_t n = i + (sync ? 50U : 72U); + if (i >= 2U) n += 72U; + if (i >= 4U) n += 96U; + if (i >= 6U) n += 72U; + WRITE_BIT(data, n, READ_BIT(duid, i)); + } + + if (m_p2DUID == P2_DUID::FACCH_SCRAMBLED || m_p2DUID == P2_DUID::SACCH_SCRAMBLED) + applyP2Scrambler(data, true, sync); +} + /* Decode a xOEMI VCH MAC PDU. */ bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync) @@ -935,6 +978,16 @@ bool LC::isStandardMFId() const return false; } +uint32_t LC::getP2MCOData(uint8_t* data, uint32_t length) const +{ + if (data == nullptr || length == 0U || p2MCOData == nullptr) + return 0U; + + uint32_t count = length < m_p2MCODataLength ? length : m_p2MCODataLength; + ::memcpy(data, p2MCOData, count); + return count; +} + /* Set the Phase 2 scrambler superframe bit offset. */ void LC::setP2ScrambleOffset(uint16_t offset) @@ -1262,7 +1315,7 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength) ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); ::memcpy(m_mi, raw + 1U, MI_LENGTH_BYTES); // Message Indicator - m_kId = (raw[10U] << 8) + raw[11U]; // Key ID + m_kId = static_cast(raw[11U] | (raw[12U] << 8U)); // Key ID if (!m_encrypted) { m_encryptOverride = true; m_encrypted = true; @@ -1333,6 +1386,9 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength) m_srcId = GET_UINT24(raw, 5U); // Source/Target Address } break; + case P2_MAC_MCO::MAC_RELEASE: + m_srcId = GET_UINT24(raw, 3U); // Source Radio Address + break; case P2_MAC_MCO::PDU_NULL: break; @@ -1348,11 +1404,9 @@ bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength) delete[] p2MCOData; uint32_t macLengthBytes = (macLength / 8U) + ((macLength % 8U) ? 1U : 0U); - p2MCOData = new uint8_t[macLengthBytes]; - ::memset(p2MCOData, 0x00U, macLengthBytes); - - // this will include the entire MCO (and depending on message length multiple MCOs) - ::memcpy(p2MCOData, raw + 1U, macLengthBytes - 3U); // excluding MAC PDU header and CRC + m_p2MCODataLength = macLengthBytes - 3U; + p2MCOData = new uint8_t[m_p2MCODataLength]; + ::memcpy(p2MCOData, raw + 1U, m_p2MCODataLength); } break; @@ -1599,14 +1653,15 @@ void LC::copy(const LC& data) if (p2MCOData != nullptr) delete[] p2MCOData; - p2MCOData = new uint8_t[P25_P2_IOEMI_MAC_LENGTH_BYTES]; - ::memset(p2MCOData, 0x00U, P25_P2_IOEMI_MAC_LENGTH_BYTES); - ::memcpy(p2MCOData, data.p2MCOData, P25_P2_IOEMI_MAC_LENGTH_BYTES); + m_p2MCODataLength = data.m_p2MCODataLength; + p2MCOData = new uint8_t[m_p2MCODataLength]; + ::memcpy(p2MCOData, data.p2MCOData, m_p2MCODataLength); } else { if (p2MCOData != nullptr) { delete[] p2MCOData; p2MCOData = nullptr; } + m_p2MCODataLength = 0U; } s_siteData = data.s_siteData; @@ -1725,46 +1780,26 @@ void LC::encodeHDUGolay(uint8_t* data, const uint8_t* raw) void LC::decodeP2_DUIDHamming(const uint8_t* data, uint8_t* raw) { - uint32_t n = 0U; - uint32_t m = 0U; - for (uint32_t i = 0U; i < 4U; i++) { - bool hamming[8U]; - - for (uint32_t j = 0U; j < 8U; j++) { - hamming[j] = READ_BIT(data, n); - n++; - } - - edac::Hamming::decode844(hamming); + bool hamming[8U]; + for (uint32_t i = 0U; i < 8U; i++) + hamming[i] = READ_BIT(data, i); - for (uint32_t j = 0U; j < 4U; j++) { - WRITE_BIT(raw, m, hamming[j]); - m++; - } - } + edac::Hamming::decode844(hamming); + for (uint32_t i = 0U; i < 4U; i++) + WRITE_BIT(raw, i, hamming[i]); } /* Encode Phase 2 DUID hamming FEC. */ void LC::encodeP2_DUIDHamming(uint8_t* data, const uint8_t* raw) { - uint32_t n = 0U; - uint32_t m = 0U; - for (uint32_t i = 0U; i < 4U; i++) { - bool hamming[8U]; - - for (uint32_t j = 0U; j < 4U; j++) { - hamming[j] = READ_BIT(raw, m); - m++; - } - - edac::Hamming::encode844(hamming); + bool hamming[8U] = { false }; + for (uint32_t i = 0U; i < 4U; i++) + hamming[i] = READ_BIT(raw, i); - for (uint32_t j = 0U; j < 8U; j++) { - WRITE_BIT(data, n, hamming[j]); - n++; - } - } + edac::Hamming::encode844(hamming); + for (uint32_t i = 0U; i < 8U; i++) + WRITE_BIT(data, i, hamming[i]); } /* Apply the Phase 2 TDMA MAC burst scrambler. */ @@ -1783,9 +1818,9 @@ void LC::applyP2Scrambler(uint8_t* data, bool inbound, bool sync) uint16_t interFieldSkips[3U] = { 0U, 0U, 0U }; if (inbound) { - cursor = 14U; + cursor = sync ? 14U : 0U; fieldCount = 4U; - fieldLengths[0U] = 36U; + fieldLengths[0U] = sync ? 36U : 72U; fieldLengths[1U] = 72U; fieldLengths[2U] = 96U; fieldLengths[3U] = 72U; diff --git a/src/common/p25/lc/LC.h b/src/common/p25/lc/LC.h index b7c0dbef..57fd39e1 100644 --- a/src/common/p25/lc/LC.h +++ b/src/common/p25/lc/LC.h @@ -111,7 +111,7 @@ namespace p25 */ void encodeLDU2(uint8_t* data); - /** Project 25 Phase II (TIA-102.BBAD-D Section 2) */ + /** Project 25 Phase II (TIA-102.BBAD-A Section 2) */ /** * @brief Decode a IEMI VCH MAC PDU. @@ -120,6 +120,8 @@ namespace p25 * @return bool True, if MAC PDU decoded, otherwise false. */ bool decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync); + /** @brief Encode an inbound/IEMI Phase 2 VCH MAC PDU. */ + void encodeVCH_MACPDU_IEMI(uint8_t* data, bool sync); /** * @brief Decode a xOEMI VCH MAC PDU. * @param data Buffer containing the MAC PDU to decode. @@ -350,7 +352,9 @@ namespace p25 /** @} */ /** @name Phase 2 Raw MCO Data */ - uint8_t* p2MCOData; // ?? - this should probably be private with getters/setters + uint8_t* p2MCOData; + uint32_t getP2MCOData(uint8_t* data, uint32_t length) const; + uint32_t getP2MCODataLength() const { return m_p2MCODataLength; } /** @} */ private: @@ -373,6 +377,7 @@ namespace p25 bool m_gotUserAliasPartA; bool m_gotUserAlias; + uint32_t m_p2MCODataLength; static bool s_warnCRC; diff --git a/src/common/p25/lc/MACPDU.cpp b/src/common/p25/lc/MACPDU.cpp new file mode 100644 index 00000000..6bc062df --- /dev/null +++ b/src/common/p25/lc/MACPDU.cpp @@ -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)"); +} diff --git a/src/common/p25/lc/MACPDU.h b/src/common/p25/lc/MACPDU.h new file mode 100644 index 00000000..f7fbfa1a --- /dev/null +++ b/src/common/p25/lc/MACPDU.h @@ -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 + +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__ diff --git a/src/common/p25/lc/mac/MACFactory.cpp b/src/common/p25/lc/mac/MACFactory.cpp new file mode 100644 index 00000000..1ef40821 --- /dev/null +++ b/src/common/p25/lc/mac/MACFactory.cpp @@ -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 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 MACFactory::decode(MACPDU* mac, const LC& control) +{ + if (mac == nullptr || !mac->decode(control)) { + delete mac; + return nullptr; + } + + return std::unique_ptr(mac); +} diff --git a/src/common/p25/lc/mac/MACFactory.h b/src/common/p25/lc/mac/MACFactory.h new file mode 100644 index 00000000..2dd61462 --- /dev/null +++ b/src/common/p25/lc/mac/MACFactory.h @@ -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 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 decode(MACPDU* mac, const LC& control); + }; + } + } +} + +#endif // __P25_LC_MAC__MAC_FACTORY_H__ diff --git a/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.cpp b/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.cpp new file mode 100644 index 00000000..6b89a1a7 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.cpp @@ -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)"); +} diff --git a/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.h b/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.h new file mode 100644 index 00000000..153dca46 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_GROUP_VCH_USER.h @@ -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__ diff --git a/src/common/p25/lc/mac/MAC_RELEASE.cpp b/src/common/p25/lc/mac/MAC_RELEASE.cpp new file mode 100644 index 00000000..c7da29d9 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_RELEASE.cpp @@ -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)"); +} diff --git a/src/common/p25/lc/mac/MAC_RELEASE.h b/src/common/p25/lc/mac/MAC_RELEASE.h new file mode 100644 index 00000000..5a06ac31 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_RELEASE.h @@ -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__ diff --git a/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.cpp b/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.cpp new file mode 100644 index 00000000..d313548f --- /dev/null +++ b/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.cpp @@ -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)"); +} diff --git a/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h b/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h new file mode 100644 index 00000000..054337ed --- /dev/null +++ b/src/common/p25/lc/mac/MAC_TEL_INT_VCH_USER.h @@ -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__ diff --git a/src/common/p25/lc/mac/MAC_UU_VCH_USER.cpp b/src/common/p25/lc/mac/MAC_UU_VCH_USER.cpp new file mode 100644 index 00000000..1cb90751 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_UU_VCH_USER.cpp @@ -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)"); +} diff --git a/src/common/p25/lc/mac/MAC_UU_VCH_USER.h b/src/common/p25/lc/mac/MAC_UU_VCH_USER.h new file mode 100644 index 00000000..a788c0e0 --- /dev/null +++ b/src/common/p25/lc/mac/MAC_UU_VCH_USER.h @@ -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__ \ No newline at end of file diff --git a/src/host/CMakeLists.txt b/src/host/CMakeLists.txt index 7b94bba7..1410a8ca 100644 --- a/src/host/CMakeLists.txt +++ b/src/host/CMakeLists.txt @@ -22,12 +22,16 @@ file(GLOB dvmhost_SRC # P25 module "src/host/p25/*.h" "src/host/p25/*.cpp" + "src/host/p25/phase2/*.h" + "src/host/p25/phase2/*.cpp" "src/host/p25/lc/tsbk/*.h" "src/host/p25/lc/tsbk/*.cpp" "src/host/p25/lookups/*.h" "src/host/p25/lookups/*.cpp" "src/host/p25/packet/*.h" "src/host/p25/packet/*.cpp" + "src/host/p25/packet/phase2/*.h" + "src/host/p25/packet/phase2/*.cpp" # NXDN module "src/host/nxdn/*.h" diff --git a/src/host/modem/Modem.cpp b/src/host/modem/Modem.cpp index da431cc8..bf375b58 100644 --- a/src/host/modem/Modem.cpp +++ b/src/host/modem/Modem.cpp @@ -129,6 +129,7 @@ Modem::Modem(port::IModemPort* port, bool duplex, bool rxInvert, bool txInvert, m_adcOverFlowCount(0U), m_dacOverFlowCount(0U), m_v24Connected(false), + m_p25P2Capable(false), m_modemState(STATE_IDLE), m_buffer(nullptr), m_length(0U), @@ -142,12 +143,16 @@ Modem::Modem(port::IModemPort* port, bool duplex, bool rxInvert, bool txInvert, m_rxDMRQueue1(dmrQueueSize, "Modem RX DMR1"), m_rxDMRQueue2(dmrQueueSize, "Modem RX DMR2"), m_rxP25Queue(p25QueueSize, "Modem RX P25"), + m_rxP25P2Queue1(p25QueueSize, "Modem RX P25 P2 Slot 1"), + m_rxP25P2Queue2(p25QueueSize, "Modem RX P25 P2 Slot 2"), m_rxNXDNQueue(nxdnQueueSize, "Modem RX NXDN"), m_statusTimer(1000U, 0U, MODEM_POLL_TIME), m_inactivityTimer(1000U, 8U), m_dmrSpace1(0U), m_dmrSpace2(0U), m_p25Space(0U), + m_p25P2Space1(0U), + m_p25P2Space2(0U), m_nxdnSpace(0U), m_tx(false), m_cd(false), @@ -697,6 +702,117 @@ void Modem::clock(uint32_t ms) } break; + /** Project 25 Phase 2 */ + case CMD_P25_P2_DATA1: + { + if (m_p25Enabled) { + if (!m_p25P2Capable) { + break; + } + + std::lock_guard lock(m_p25P2ReadLock1); + + if (m_rspDoubleLength) { + LogError(LOG_MODEM, "CMD_P25_P2_DATA1 double length?; len = %u", m_length); + break; + } + // Firmware supplies DUID transport metadata followed by the + // 40-byte logical burst. The DUID is not part of the air burst. + if (m_length != (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U)) { + LogError(LOG_MODEM, "CMD_P25_P2_DATA1 invalid length; len = %u", m_length); + break; + } + + uint8_t data = m_length - 2U; + m_rxP25P2Queue1.addData(&data, 1U); + + data = TAG_DATA; + m_rxP25P2Queue1.addData(&data, 1U); + + m_rxP25P2Queue1.addData(m_buffer + 3U, m_length - 3U); + if (m_trace) + Utils::dump(1U, "Modem::clock(), RX P25 P2 Data 1", m_buffer + 3U, m_length - 3U); + } + } + break; + + case CMD_P25_P2_DATA2: + { + if (m_p25Enabled) { + if (!m_p25P2Capable) { + break; + } + + std::lock_guard lock(m_p25P2ReadLock2); + + if (m_rspDoubleLength) { + LogError(LOG_MODEM, "CMD_P25_P2_DATA2 double length?; len = %u", m_length); + break; + } + if (m_length != (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U)) { + LogError(LOG_MODEM, "CMD_P25_P2_DATA2 invalid length; len = %u", m_length); + break; + } + + uint8_t data = m_length - 2U; + m_rxP25P2Queue2.addData(&data, 1U); + + data = TAG_DATA; + m_rxP25P2Queue2.addData(&data, 1U); + + m_rxP25P2Queue2.addData(m_buffer + 3U, m_length - 3U); + if (m_trace) + Utils::dump(1U, "Modem::clock(), RX P25 P2 Data 2", m_buffer + 3U, m_length - 3U); + } + } + break; + + case CMD_P25_P2_LOST1: + { + if (m_p25Enabled) { + if (!m_p25P2Capable) { + break; + } + + std::lock_guard lock(m_p25P2ReadLock1); + + if (m_rspDoubleLength) { + LogError(LOG_MODEM, "CMD_P25_P2_LOST1 double length?; len = %u", m_length); + break; + } + + uint8_t data = 1U; + m_rxP25P2Queue1.addData(&data, 1U); + + data = TAG_LOST; + m_rxP25P2Queue1.addData(&data, 1U); + } + } + break; + + case CMD_P25_P2_LOST2: + { + if (m_p25Enabled) { + if (!m_p25P2Capable) { + break; + } + + std::lock_guard lock(m_p25P2ReadLock2); + + if (m_rspDoubleLength) { + LogError(LOG_MODEM, "CMD_P25_P2_LOST2 double length?; len = %u", m_length); + break; + } + + uint8_t data = 1U; + m_rxP25P2Queue2.addData(&data, 1U); + + data = TAG_LOST; + m_rxP25P2Queue2.addData(&data, 1U); + } + } + break; + /** Next Generation Digital Narrowband */ case CMD_NXDN_DATA: { @@ -872,6 +988,13 @@ void Modem::clock(uint32_t ms) LogWarning(LOG_MODEM, "NAK, %s, p25Space = %u", rsnToString(m_buffer[4U]).c_str(), m_p25Space); break; + case CMD_P25_P2_DATA1: + LogWarning(LOG_MODEM, "NAK, %s, p25P2Space1 = %u", rsnToString(m_buffer[4U]).c_str(), m_p25P2Space1); + break; + case CMD_P25_P2_DATA2: + LogWarning(LOG_MODEM, "NAK, %s, p25P2Space2 = %u", rsnToString(m_buffer[4U]).c_str(), m_p25P2Space2); + break; + case CMD_NXDN_DATA: LogWarning(LOG_MODEM, "NAK, %s, nxdnSpace = %u", rsnToString(m_buffer[4U]).c_str(), m_nxdnSpace); break; @@ -1092,6 +1215,130 @@ uint32_t Modem::readP25Frame(uint8_t* data) return 0U; } +/* Get the frame data length for the next P25 Phase 2 Slot 1 frame. */ + +uint32_t Modem::peekP25P2Frame1Length() +{ + if (!m_p25P2Capable) + return 0U; + + std::lock_guard lock(m_p25P2ReadLock1); + if (m_rxP25P2Queue1.isEmpty()) + return 0U; + + uint8_t len = 0U; + m_rxP25P2Queue1.peek(&len, 1U); +#if DEBUG_MODEM + LogDebugEx(LOG_MODEM, "Modem::peekP25P2Frame1Length()", "len = %u, dataSize = %u", len, m_rxP25P2Queue2.dataSize()); +#endif + // this ensures we never get in a situation where we have length stuck on the queue + if (m_rxP25P2Queue1.dataSize() == 1U && len > m_rxP25P2Queue1.dataSize()) { + m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off + return 0U; + } + + if (m_rxP25P2Queue1.dataSize() > len) { + return len; + } + + return 0U; +} + +/* Reads P25 Phase 2 Slot 1 frame data. */ + +uint32_t Modem::readP25P2Frame1(uint8_t* data) +{ + assert(data != nullptr); + + if (!m_p25P2Capable) + return 0U; + + std::lock_guard lock(m_p25P2ReadLock1); + + if (m_rxP25P2Queue1.isEmpty()) + return 0U; + + uint8_t len = 0U; + m_rxP25P2Queue1.peek(&len, 1U); + + // this ensures we never get in a situation where we have length stuck on the queue + if (m_rxP25P2Queue1.dataSize() == 1U && len > m_rxP25P2Queue1.dataSize()) { + m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off + return 0U; + } + + if (m_rxP25P2Queue1.dataSize() > len) { + m_rxP25P2Queue1.get(&len, 1U); // ensure we pop the length off + m_rxP25P2Queue1.get(data, len); + + return len; + } + + return 0U; +} + +/* Get the frame data length for the next P25 Phase 2 Slot 2 frame. */ + +uint32_t Modem::peekP25P2Frame2Length() +{ + if (!m_p25P2Capable) + return 0U; + + std::lock_guard lock(m_p25P2ReadLock2); + if (m_rxP25P2Queue2.isEmpty()) + return 0U; + + uint8_t len = 0U; + m_rxP25P2Queue2.peek(&len, 1U); +#if DEBUG_MODEM + LogDebugEx(LOG_MODEM, "Modem::peekP25P2Frame2Length()", "len = %u, dataSize = %u", len, m_rxP25P2Queue2.dataSize()); +#endif + // this ensures we never get in a situation where we have length stuck on the queue + if (m_rxP25P2Queue2.dataSize() == 1U && len > m_rxP25P2Queue2.dataSize()) { + m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off + return 0U; + } + + if (m_rxP25P2Queue2.dataSize() > len) { + return len; + } + + return 0U; +} + +/* Reads P25 Phase 2 Slot 2 frame data. */ + +uint32_t Modem::readP25P2Frame2(uint8_t* data) +{ + assert(data != nullptr); + + if (!m_p25P2Capable) + return 0U; + + std::lock_guard lock(m_p25P2ReadLock2); + + if (m_rxP25P2Queue2.isEmpty()) + return 0U; + + uint8_t len = 0U; + m_rxP25P2Queue2.peek(&len, 1U); + + // this ensures we never get in a situation where we have length stuck on the queue + if (m_rxP25P2Queue2.dataSize() == 1U && len > m_rxP25P2Queue2.dataSize()) { + m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off + return 0U; + } + + if (m_rxP25P2Queue2.dataSize() > len) { + m_rxP25P2Queue2.get(&len, 1U); // ensure we pop the length off + m_rxP25P2Queue2.get(data, len); + + return len; + } + + return 0U; +} + /* Get the frame data length for the next frame in the NXDN ring buffer. */ uint32_t Modem::peekNXDNFrameLength() @@ -1167,6 +1414,20 @@ bool Modem::hasP25Space(uint32_t length) const return m_p25Space >= length; } +/* Helper to test if the P25 Phase 2 Slot 1 ring buffer has free space. */ + +bool Modem::hasP25P2Space1() const +{ + return m_p25P2Space1 >= (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 2U); +} + +/* Helper to test if the P25 Phase 2 Slot 2 ring buffer has free space. */ + +bool Modem::hasP25P2Space2() const +{ + return m_p25P2Space2 >= (P25DEF::P25_P2_FRAME_LENGTH_BYTES + 2U); +} + /* Helper to test if the NXDN ring buffer has free space. */ bool Modem::hasNXDNSpace() const @@ -1264,6 +1525,44 @@ void Modem::clearP25Frame() Thread::sleep(5); // 5ms delay } +/* Clears any buffered P25 Phase 2 Slot 1 frame data to be sent to the air interface modem. */ + +void Modem::clearP25P2Frame1() +{ + if (!m_p25P2Capable) + return; + + uint8_t buffer[3U]; + + buffer[0U] = DVM_SHORT_FRAME_START; + buffer[1U] = 3U; + buffer[2U] = CMD_P25_P2_CLEAR1; +#if DEBUG_MODEM + Utils::dump(1U, "Modem::clearP25P2Frame1(), Written", buffer, 3U); +#endif + write(buffer, 3U); + Thread::sleep(5); // 5ms delay +} + +/* Clears any buffered P25 Phase 2 Slot 2 frame data to be sent to the air interface modem. */ + +void Modem::clearP25P2Frame2() +{ + if (!m_p25P2Capable) + return; + + uint8_t buffer[3U]; + + buffer[0U] = DVM_SHORT_FRAME_START; + buffer[1U] = 3U; + buffer[2U] = CMD_P25_P2_CLEAR2; +#if DEBUG_MODEM + Utils::dump(1U, "Modem::clearP25P2Frame2(), Written", buffer, 3U); +#endif + write(buffer, 3U); + Thread::sleep(5); // 5ms delay +} + /* Clears any buffered NXDN frame data to be sent to the air interface modem. */ void Modem::clearNXDNFrame() @@ -1349,6 +1648,62 @@ void Modem::injectP25Frame(const uint8_t* data, uint32_t length) } } +/* Internal helper to inject P25 Phase 2 Slot 1 frame data. */ + +void Modem::injectP25P2Frame1(const uint8_t* data, uint32_t length) +{ + assert(data != nullptr); + assert(length > 0U); + + if (m_p25Enabled) { + if (!m_p25P2Capable) + return; + + if (m_trace) + Utils::dump(1U, "Injected P25 Phase 2 Slot 1 Data", data, length); + + uint8_t val = length + 2U; + m_rxP25P2Queue1.addData(&val, 1U); + + val = TAG_DATA; + m_rxP25P2Queue1.addData(&val, 1U); + + // Injection defaults to a 4V logical voice burst. Tests needing MAC + // signaling should inject the complete typed modem envelope instead. + val = P25DEF::P2_DUID::VTCH_4V; + m_rxP25P2Queue1.addData(&val, 1U); + + m_rxP25P2Queue1.addData(data, length); + } +} + +/* Internal helper to inject P25 Phase 2 Slot 2 frame data. */ + +void Modem::injectP25P2Frame2(const uint8_t* data, uint32_t length) +{ + assert(data != nullptr); + assert(length > 0U); + + if (m_p25Enabled) { + if (!m_p25P2Capable) + return; + + if (m_trace) + Utils::dump(1U, "Injected P25 Phase 2 Slot 2 Data", data, length); + + uint8_t val = length + 2U; + m_rxP25P2Queue2.addData(&val, 1U); + + val = TAG_DATA; + m_rxP25P2Queue2.addData(&val, 1U); + + val = P25DEF::P2_DUID::VTCH_4V; + m_rxP25P2Queue2.addData(&val, 1U); + + m_rxP25P2Queue2.addData(data, length); + } +} + /* Internal helper to inject NXDN frame data as if it came from the air interface modem. */ void Modem::injectNXDNFrame(const uint8_t* data, uint32_t length) @@ -1562,6 +1917,137 @@ bool Modem::writeP25Frame(const uint8_t* data, uint32_t length, bool imm) } } +/* Writes P25 Phase 2 Slot 1 frame data to the P25 Phase 2 Slot 1 ring buffer. */ + +bool Modem::writeP25P2Frame1(const uint8_t* data, uint32_t length, bool imm) +{ + assert(data != nullptr); + assert(length > 0U); + + if (m_p25Enabled) { + if (!m_p25P2Capable) + return false; + + const uint8_t MAX_LENGTH = 48U; + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + if (length > MAX_LENGTH) { + LogError(LOG_MODEM, "Modem::writeP25P2Frame1(); request data to write >%u?, len = %u", MAX_LENGTH, length); + Utils::dump(1U, "Modem::writeP25P2Frame1(), Attempted Data", data, length); + return false; + } + + uint8_t buffer[MAX_LENGTH]; + + buffer[0U] = DVM_SHORT_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = CMD_P25_P2_DATA1; + + // The serial payload is DUID + 40-byte logical burst. Firmware uses the + // DUID to select a legal voice/FACCH/SACCH opportunity; it must not air + // the metadata octet. + // TODO(P25P2-FW): Implement the typed per-LCH scheduler, ISCH insertion, + // superframe/ultraframe tracking, and scrambler phase in modem firmware. + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + uint8_t len = length + 2U; + + // write or buffer P25 Phase 2 slot 1 data to air interface + if (m_p25P2Space1 >= length) { + if (m_debug) + LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame1()", "immediate write (len %u)", length); + if (m_trace) + Utils::dump(1U, "Modem::writeP25P2Frame1(), Immediate TX P25 Phase 2 Slot 1 Data", buffer + 3U, length - 1U); + + int ret = write(buffer, len, imm); + if (ret != int(len)) { + LogError(LOG_MODEM, "Error writing P25 Phase 2 Slot 1 data"); + return false; + } + + m_p25P2Space1 -= length; + if ((int32_t)m_p25P2Space1 < 0) { + if (m_debug) + LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame1()", "m_p25P2Space1 underflow, space = %u, length = %u", m_p25P2Space1, length); + m_p25P2Space1 = 0U; + } + } + else { + return false; + } + + return true; + } + else { + return false; + } +} + +/* Writes P25 Phase 2 Slot 2 frame data to the P25 Phase 2 Slot 2 ring buffer. */ + +bool Modem::writeP25P2Frame2(const uint8_t* data, uint32_t length, bool imm) +{ + assert(data != nullptr); + assert(length > 0U); + + if (m_p25Enabled) { + if (!m_p25P2Capable) + return false; + + const uint8_t MAX_LENGTH = 48U; + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + if (length > MAX_LENGTH) { + LogError(LOG_MODEM, "Modem::writeP25P2Frame2(); request data to write >%u?, len = %u", MAX_LENGTH, length); + Utils::dump(1U, "Modem::writeP25P2Frame2(), Attempted Data", data, length); + return false; + } + + uint8_t buffer[MAX_LENGTH]; + + buffer[0U] = DVM_SHORT_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = CMD_P25_P2_DATA2; + + // TODO(P25P2-FW): Treat immediate as "next legal opportunity for this + // DUID", never as permission to violate FACCH/SACCH/LCH placement. + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + uint8_t len = length + 2U; + + // write or buffer P25 Phase 2 slot 2 data to air interface + if (m_p25P2Space2 >= length) { + if (m_debug) + LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame2()", "immediate write (len %u)", length); + if (m_trace) + Utils::dump(1U, "Modem::writeP25P2Frame2(), Immediate TX P25 Phase 2 Slot 2 Data", buffer + 3U, length - 1U); + + int ret = write(buffer, len, imm); + if (ret != int(len)) { + LogError(LOG_MODEM, "Error writing P25 Phase 2 Slot 2 data"); + return false; + } + + m_p25P2Space2 -= length; + if ((int32_t)m_p25P2Space2 < 0) { + if (m_debug) + LogDebugEx(LOG_MODEM, "Modem::writeP25P2Frame2()", "m_p25P2Space2 underflow, space = %u, length = %u", m_p25P2Space2, length); + m_p25P2Space2 = 0U; + } + } + else { + return false; + } + + return true; + } + else { + return false; + } +} + /* Writes NXDN frame data to the NXDN ring buffer. */ bool Modem::writeNXDNFrame(const uint8_t* data, uint32_t length, bool imm) diff --git a/src/host/modem/Modem.h b/src/host/modem/Modem.h index 83bd70a7..448d4590 100644 --- a/src/host/modem/Modem.h +++ b/src/host/modem/Modem.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2011-2021 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * Copyright (C) 2021 Nat Moore * */ @@ -28,6 +28,7 @@ #include "Defines.h" #include "common/RingBuffer.h" +#include "common/p25/P25Defines.h" #include "common/Timer.h" #include "modem/port/IModemPort.h" #include "restapi/RESTAPI.h" @@ -172,6 +173,13 @@ namespace modem CMD_P25_LOST = 0x32U, //!< Project 25 Data Lost CMD_P25_CLEAR = 0x33U, //!< Project 25 Clear Buffer + CMD_P25_P2_DATA1 = 0x34U, //!< Project 25 Phase 2 Data Slot 1 + CMD_P25_P2_LOST1 = 0x35U, //!< Project 25 Phase 2 Data Lost Slot 1 + CMD_P25_P2_DATA2 = 0x36U, //!< Project 25 Phase 2 Data Slot 2 + CMD_P25_P2_LOST2 = 0x37U, //!< Project 25 Phase 2 Data Lost Slot 2 + CMD_P25_P2_CLEAR1 = 0x38U, //!< Project 25 Phase 2 Clear Slot 1 Buffer + CMD_P25_P2_CLEAR2 = 0x39U, //!< Project 25 Phase 2 Clear Slot 2 Buffer + CMD_NXDN_DATA = 0x41U, //!< NXDN Data CMD_NXDN_LOST = 0x42U, //!< NXDN Data Lost CMD_NXDN_CLEAR = 0x43U, //!< NXDN Clear Buffer @@ -469,6 +477,28 @@ namespace modem * @returns uint32_t Length of data read from ring buffer. */ uint32_t readP25Frame(uint8_t* data); + /** + * @brief Get the frame data length for the next P25 Phase 2 Slot 1 frame. + * @returns uint32_t Length of frame data retrieved. + */ + uint32_t peekP25P2Frame1Length(); + /** + * @brief Reads P25 Phase 2 Slot 1 frame data. + * @param[out] data Buffer to write frame data to. + * @returns uint32_t Length of data read from ring buffer. + */ + uint32_t readP25P2Frame1(uint8_t* data); + /** + * @brief Get the frame data length for the next P25 Phase 2 Slot 2 frame. + * @returns uint32_t Length of frame data retrieved. + */ + uint32_t peekP25P2Frame2Length(); + /** + * @brief Reads P25 Phase 2 Slot 2 frame data. + * @param[out] data Buffer to write frame data to. + * @returns uint32_t Length of data read from ring buffer. + */ + uint32_t readP25P2Frame2(uint8_t* data); /** * @brief Get the frame data length for the next frame in the NXDN ring buffer. * @returns uint32_t Length of frame data retrieved. @@ -511,6 +541,26 @@ namespace modem * @return uint32_t Size in bytes available for the P25 ring buffer. */ uint32_t getP25Space() const { return m_p25Space; } + /** + * @brief Helper to test if the P25 Phase 2 Slot 1 ring buffer has free space. + * @returns bool True, if the P25 Phase 2 Slot 1 ring buffer has free space, otherwise false. + */ + bool hasP25P2Space1() const; + /** + * @brief Helper to return the currently reported available P25 Phase 2 Slot 1 ring buffer free space. + * @return uint32_t Size in bytes available for the P25 Phase 2 Slot 1 ring buffer. + */ + uint32_t getP25P2Space1() const { return m_p25P2Space1; } + /** + * @brief Helper to test if the P25 Phase 2 Slot 2 ring buffer has free space. + * @returns bool True, if the P25 Phase 2 Slot 2 ring buffer has free space, otherwise false. + */ + bool hasP25P2Space2() const; + /** + * @brief Helper to return the currently reported available P25 Phase 2 Slot 2 ring buffer free space. + * @return uint32_t Size in bytes available for the P25 Phase 2 Slot 2 ring buffer. + */ + uint32_t getP25P2Space2() const { return m_p25P2Space2; } /** * @brief Helper to test if the NXDN ring buffer has free space. * @returns bool True, if the NXDN ring buffer has free space, otherwise false. @@ -568,6 +618,14 @@ namespace modem * @brief Clears any buffered P25 frame data to be sent to the air interface modem. */ void clearP25Frame(); + /** + * @brief Clears any buffered P25 Phase 2 Slot 1 frame data to be sent to the air interface modem. + */ + void clearP25P2Frame1(); + /** + * @brief Clears any buffered P25 Phase 2 Slot 2 frame data to be sent to the air interface modem. + */ + void clearP25P2Frame2(); /** * @brief Clears any buffered NXDN frame data to be sent to the air interface modem. */ @@ -591,6 +649,18 @@ namespace modem * @param length Length of data to write. */ void injectP25Frame(const uint8_t* data, uint32_t length); + /** + * @brief Internal helper to inject P25 Phase 2 Slot 1 frame data. + * @param[in] data Data to write to ring buffer. + * @param length Length of data to write. + */ + void injectP25P2Frame1(const uint8_t* data, uint32_t length); + /** + * @brief Internal helper to inject P25 Phase 2 Slot 2 frame data. + * @param[in] data Data to write to ring buffer. + * @param length Length of data to write. + */ + void injectP25P2Frame2(const uint8_t* data, uint32_t length); /** * @brief Internal helper to inject NXDN frame data as if it came from the air interface modem. * @param[in] data Data to write to ring buffer. @@ -622,6 +692,22 @@ namespace modem * @returns bool True, if data is written, otherwise false. */ bool writeP25Frame(const uint8_t* data, uint32_t length, bool imm = false); + /** + * @brief Write P25 Phase 2 Slot 1 frame data to the P25 Phase 2 Slot 1 ring buffer. + * @param[in] data Data to write to ring buffer. + * @param length Length of data to write. + * @param imm Flag indicating whether the frame is immediate. + * @returns bool True, if data is written, otherwise false. + */ + bool writeP25P2Frame1(const uint8_t* data, uint32_t length, bool imm = false); + /** + * @brief Writes P25 Phase 2 Slot 2 frame data to the P25 Phase 2 Slot 2 ring buffer. + * @param[in] data Data to write to ring buffer. + * @param length Length of data to write. + * @param imm Flag indicating whether the frame is immediate. + * @returns bool True, if data is written, otherwise false. + */ + bool writeP25P2Frame2(const uint8_t* data, uint32_t length, bool imm = false); /** * @brief Writes NXDN frame data to the NXDN ring buffer. * @param[in] data Data to write to ring buffer. @@ -791,6 +877,7 @@ namespace modem uint32_t m_dacOverFlowCount; // dedicated modem - DAC overflow count bool m_v24Connected; + bool m_p25P2Capable; DVM_STATE m_modemState; uint8_t* m_buffer; @@ -807,6 +894,8 @@ namespace modem RingBuffer m_rxDMRQueue1; RingBuffer m_rxDMRQueue2; RingBuffer m_rxP25Queue; + RingBuffer m_rxP25P2Queue1; + RingBuffer m_rxP25P2Queue2; RingBuffer m_rxNXDNQueue; Timer m_statusTimer; @@ -815,6 +904,8 @@ namespace modem uint32_t m_dmrSpace1; uint32_t m_dmrSpace2; uint32_t m_p25Space; + uint32_t m_p25P2Space1; + uint32_t m_p25P2Space2; uint32_t m_nxdnSpace; bool m_tx; @@ -825,6 +916,8 @@ namespace modem std::mutex m_dmr1ReadLock; std::mutex m_dmr2ReadLock; std::mutex m_p25ReadLock; + std::mutex m_p25P2ReadLock1; + std::mutex m_p25P2ReadLock2; std::mutex m_nxdnReadLock; bool m_ignoreModemConfigArea; diff --git a/src/host/p25/packet/phase2/Voice.cpp b/src/host/p25/packet/phase2/Voice.cpp new file mode 100644 index 00000000..8a27a0f1 --- /dev/null +++ b/src/host/p25/packet/phase2/Voice.cpp @@ -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 + +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; +} diff --git a/src/host/p25/packet/phase2/Voice.h b/src/host/p25/packet/phase2/Voice.h new file mode 100644 index 00000000..4ec0241c --- /dev/null +++ b/src/host/p25/packet/phase2/Voice.h @@ -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 + +// --------------------------------------------------------------------------- +// 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__ diff --git a/src/host/p25/phase2/Control.cpp b/src/host/p25/phase2/Control.cpp new file mode 100644 index 00000000..a14b0c30 --- /dev/null +++ b/src/host/p25/phase2/Control.cpp @@ -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 + +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; +} diff --git a/src/host/p25/phase2/Control.h b/src/host/p25/phase2/Control.h new file mode 100644 index 00000000..f8eaeec3 --- /dev/null +++ b/src/host/p25/phase2/Control.h @@ -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 + +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__ diff --git a/src/host/p25/phase2/Slot.cpp b/src/host/p25/phase2/Slot.cpp new file mode 100644 index 00000000..3e5ef8d4 --- /dev/null +++ b/src/host/p25/phase2/Slot.cpp @@ -0,0 +1,1230 @@ +// 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/lc/mac/MACFactory.h" +#include "common/p25/acl/AccessControl.h" +#include "common/Log.h" +#include "common/Utils.h" +#include "p25/phase2/Slot.h" +#include "p25/phase2/Control.h" +#include "ActivityLog.h" + +using namespace p25::phase2; +using namespace p25::defines; + +// --------------------------------------------------------------------------- +// Macros +// --------------------------------------------------------------------------- + +// Enforces authoritative ownership and RF RID/TGID policy before forwarding. +#define CHECK_AUTHORITATIVE(_SRC_ID, _DST_ID, _GROUP) \ + do { \ + if (!validateRFCall((_SRC_ID), (_DST_ID), (_GROUP))) \ + return false; \ + } while (0) + +// Enforces the per-slot permit received from an authoritative P25 CC. +#define CHECK_NET_AUTHORITATIVE(_DST_ID) \ + do { \ + if (!s_authoritative && m_permittedDstId != (_DST_ID)) { \ + if (m_debug) \ + LogDebugEx(LOG_NET, "Slot::processNetwork()", \ + "P25 Phase 2 Slot %u, network destination not permitted, dstId = %u", \ + m_slotNo + 1U, (_DST_ID)); \ + ++m_netMissed; \ + return false; \ + } \ + } while (0) + +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +::p25::phase2::Control* Slot::s_control = nullptr; + +bool Slot::s_authoritative = true; + +uint32_t Slot::s_callHang = 0U; + +modem::Modem* Slot::s_modem = nullptr; +network::Network* Slot::s_network = nullptr; + +::p25::lookups::P25AffiliationLookup* Slot::s_affiliations = nullptr; +::lookups::RadioIdLookup* Slot::s_ridLookup = nullptr; +::lookups::TalkgroupRulesLookup* Slot::s_tidLookup = nullptr; + + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a Phase 2 slot and its packet processors. */ + +Slot::Slot(uint32_t slotNo, uint32_t timeout, uint32_t tgHang, uint32_t queueSize, bool debug, bool verbose) : + m_voice(this, debug, verbose), + m_slotNo(slotNo), + m_txQueue(queueSize, "P25 Phase 2 Slot Frame"), + m_txImmQueue(queueSize, "P25 Phase 2 Slot Immediate Frame"), + m_rfState(RS_RF_LISTENING), + m_rfLastDstId(0U), + m_rfLastSrcId(0U), + m_netState(RS_NET_IDLE), + m_netLastDstId(0U), + m_netLastSrcId(0U), + m_permittedDstId(0U), + m_control(), + m_rfResetPending(false), + m_netResetPending(false), + m_rfVCHState(VCH_STATE::IDLE), + m_netVCHState(VCH_STATE::IDLE), + m_rfPTTCount(0U), + m_rfEndPTTCount(0U), + m_netEndPTTCount(0U), + m_rfFrames(0U), + m_netFrames(0U), + m_netLost(0U), + m_netMissed(0U), + m_rfBits(1U), + m_netBits(1U), + m_rfErrs(0U), + m_netErrs(0U), + m_rfTimeout(false), + m_netTimeout(false), + m_frameLossCnt(0U), + m_frameLossThreshold(P2_DEFAULT_FRAME_LOSS_THRESHOLD), + m_elapsedMs(0U), + m_vcuElapsedMs(0U), + m_rfTimeoutTimer(1000U, timeout), + m_rfTGHang(1000U, tgHang), + m_netTimeoutTimer(1000U, timeout), + m_netTGHang(1000U, tgHang), + m_networkWatchdog(1000U, 0U, 1500U), + m_rfLossWatchdog(1000U, 0U, 1500U), + m_rfCallHangTimer(1000U, s_callHang), + m_netCallHangTimer(1000U, s_callHang), + m_debug(debug), + m_verbose(verbose), + m_duid(defines::P2_DUID::VTCH_4V), + m_controlByte(0U), + m_rfBurstCount(0U), + m_netBurstCount(0U), + m_rfScrambleOffset(0U), + m_netScrambleOffset(0U) +{ + /* stub */ +} + +/* Resets slot state, counters, and queued frames. */ + +void Slot::reset() +{ + // scope is intentional + { + std::lock_guard lock(m_queueLock); + m_txQueue.clear(); + m_txImmQueue.clear(); + } + + resetRF(); + resetNet(); + m_rfResetPending = false; + m_netResetPending = false; + m_elapsedMs = 0U; + m_rfBurstCount = 0U; + m_netBurstCount = 0U; + m_rfScrambleOffset = 0U; + m_netScrambleOffset = 0U; + m_control = lc::LC(); + m_duid = defines::P2_DUID::VTCH_4V; + m_controlByte = 0U; +} + +/* Process a data frame from the RF interface. */ + +bool Slot::processFrame(uint8_t* data, uint32_t length) +{ + if (data == nullptr || length == 0U) + return false; + + // Network traffic owns the physical TDMA slot until its termination has + // drained. Do not allow an RF call to create two simultaneous owners. + if (m_netState != RS_NET_IDLE && data[0U] != modem::TAG_LOST) { + LogWarning(LOG_RF, "P25 Phase 2 Slot %u, RF traffic collision with active network call, srcId = %u, dstId = %u", + m_slotNo + 1U, m_netLastSrcId, m_netLastDstId); + return false; + } + + if (m_rfState == RS_RF_REJECTED) + return false; + + if (length == 1U && data[0U] == modem::TAG_LOST) { + if (m_frameLossCnt > m_frameLossThreshold) { + m_frameLossCnt = 0U; + + processFrameLoss(RF_LOSS_TYPE_EXCEEDED_FRAME_THRESHOLD); + + return false; + } + else { + // increment the frame loss count by one for audio or data; otherwise drop + // packets + if (m_rfState == RS_RF_AUDIO) { + m_rfLossWatchdog.start(); + ++m_frameLossCnt; + } + else { + // drop normally + m_frameLossCnt = 0U; + m_rfState = RS_RF_LISTENING; + + m_rfLastDstId = 0U; + m_rfLastSrcId = 0U; + m_rfTGHang.stop(); + + return false; + } + } + } + + if (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA) { + // if RF TG hang is disabled, keep the loss watchdog alive from inbound + // RF frames so abrupt stream loss can still recover state. + if (m_rfTGHang.getTimeout() == 0U || m_rfLossWatchdog.isRunning()) { + m_rfLossWatchdog.start(); + } + } + + const uint8_t* burst = data; + bool typedBurst = false; + defines::P2_DUID::E duid = defines::P2_DUID::VTCH_4V; + if (length == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES) { + if (data[0U] == modem::TAG_LOST) { + processFrameLoss(RF_LOSS_TYPE_EXCEEDED_FRAME_THRESHOLD); + return false; + } + + if (data[0U] != modem::TAG_DATA && data[0U] != modem::TAG_EOT) + return false; + + duid = static_cast(data[1U] & 0x0FU); + burst = data + 2U; + length -= 2U; + typedBurst = true; + } + + if (length != P25DEF::P25_P2_FRAME_LENGTH_BYTES) + return false; + + // TODO(P25P2-FW): The modem must own the authoritative 12-slot + // superframe counter and scrambler phase. This host-side offset is a + // compatibility aid until the typed modem scheduler is implemented. + m_rfScrambleOffset = static_cast( + ((m_rfBurstCount * 2U + m_slotNo) * P25DEF::P25_P2_BURST_LENGTH_BITS) % + P25DEF::P25_P2_SCRAMBLER_SEQUENCE_BITS); + m_control.setP2ScrambleOffset(m_rfScrambleOffset); + + if (typedBurst) { + // Modem supplies the DUID because the burst structure is needed to + // locate and decode the on-air DUID itself. Inbound FACCH/SACCH are IEMI. + if (!isVoiceDUID(duid) && !isLCCHDUID(duid) && !m_control.decodeVCH_MACPDU_IEMI(burst, isFACCHDUID(duid))) + return false; + } else { + // Compatibility path for existing tests and injected logical voice. + // TODO(P25P2-FW): Remove once all modem emits the typed envelope. + if (!m_control.decodeVCH_MACPDU_IEMI(burst, false)) + return false; + duid = static_cast(m_control.getP2DUID()); + } + + m_duid = duid; + + // LCCH is an LCH designation, not VCH-associated signaling. It requires + // IECI/OECI coding and a separate host control-channel processor. + // TODO(P25P2-LCCH): Route LCCH bursts to a dedicated LCCH controller once + // the firmware reports LCH designation and LCCH transition events. + if (isLCCHDUID(duid)) + return false; + if (!isVoiceDUID(duid) && !isFACCHDUID(duid) && !isSACCHDUID(duid)) + return false; + + const bool voice = isVoiceDUID(duid); + bool release = false; + if (voice) { + if (m_rfState == RS_RF_LISTENING) + m_rfState = RS_RF_LATE_ENTRY; + if (!m_voice.process(const_cast(burst), length)) + return false; + } else { + if (!processMAC(const_cast(burst), length, duid, false, release)) + return false; + + // handle MAC PDU opcodes for FACCH bursts + switch (m_control.getMACPDUOpcode()) { + case defines::P2_MAC_HEADER_OPCODE::PTT: + if (isFACCHDUID(duid)) { + if (m_rfVCHState == VCH_STATE::IDLE) { + m_rfFrames = 0U; + m_rfBits = 1U; + m_rfErrs = 0U; + + ::ActivityLog("P25P2", true, "Slot %u RF voice call from %u to %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + LogInfoEx(LOG_RF, "P25 Phase 2 voice call start, slot = %u, srcId = %u, dstId = %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + } + + if (m_rfVCHState == VCH_STATE::HANGTIME) { + m_rfCallHangTimer.stop(); + m_rfEndPTTCount = 0U; + m_rfResetPending = false; + } + + m_rfPTTCount++; + m_rfVCHState = VCH_STATE::PTT; + m_vcuElapsedMs = 0U; + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::PTT, true); + } + break; + case defines::P2_MAC_HEADER_OPCODE::ACTIVE: + if (m_rfPTTCount > 0U) { + m_rfVCHState = VCH_STATE::ACTIVE; + } + else if (m_rfState == RS_RF_LATE_ENTRY) { + m_rfVCHState = VCH_STATE::ACTIVE; + ::ActivityLog("P25P2", true, "Slot %u RF late entry from %u to %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + } + break; + case defines::P2_MAC_HEADER_OPCODE::END_PTT: + if (isFACCHDUID(duid)) { + m_rfEndPTTCount++; + if (m_rfEndPTTCount == 2U) { + ::ActivityLog("P25P2", true, "Slot %u RF voice call ended from %u to %u, %.1f seconds, BER: %.1f%%", + m_slotNo + 1U, m_rfLastSrcId, m_rfLastDstId, float(m_rfFrames) / 16.667F, float(m_rfErrs * 100U) / float(m_rfBits)); + LogInfoEx(LOG_RF, "P25 Phase 2 voice call end, slot = %u, srcId = %u, dstId = %u, frames = %u, bits = %u, errors = %u, BER = %.4f%%", + m_slotNo + 1U, m_rfLastSrcId, m_rfLastDstId, m_rfFrames, m_rfBits, m_rfErrs, float(m_rfErrs * 100U) / float(m_rfBits)); + beginHangtime(false); + } + } + break; + default: + break; + } + } + + if (release) + writeEnd(false); + + if (m_rfVCHState != VCH_STATE::IDLE) + m_rfState = RS_RF_AUDIO; + m_rfLastDstId = m_control.getDstId(); + m_rfLastSrcId = m_control.getSrcId(); + + touchGrant(m_rfLastDstId); + + if (!m_rfTimeoutTimer.isRunning()) + m_rfTimeoutTimer.start(); + + m_rfTGHang.start(); + m_rfLossWatchdog.start(); + m_elapsedMs = 0U; + m_rfBurstCount++; + return true; +} + +/* Returns the next queued frame length. */ + +uint32_t Slot::peekFrameLength() +{ + std::lock_guard lock(m_queueLock); + + if (m_txQueue.isEmpty() && m_txImmQueue.isEmpty()) + return 0U; + + uint8_t len = 0U; + + // tx immediate queue takes priority + if (!m_txImmQueue.isEmpty()) { + m_txImmQueue.peek(&len, 1U); + } + else { + m_txQueue.peek(&len, 1U); + } + + return len; +} + +/* Returns whether the slot queue is full. */ + +bool Slot::isQueueFull() const +{ + if (m_txQueue.isEmpty() && m_txImmQueue.isEmpty()) + return false; + + // tx immediate queue takes priority + if (!m_txImmQueue.isEmpty()) { + uint32_t space = m_txImmQueue.freeSpace(); + if (space < (P25_P2_HOST_FRAME_LENGTH_BYTES + 1U)) + return true; + } + else { + uint32_t space = m_txQueue.freeSpace(); + if (space < (P25_P2_HOST_FRAME_LENGTH_BYTES + 1U)) + return true; + } + + return false; +} + +/* Retrieves the next queued frame. */ + +uint32_t Slot::getFrame(uint8_t* data, bool* imm) +{ + if (data == nullptr) + return 0U; + + uint8_t storedLength = 0U; + bool shouldReset = false; + { + std::lock_guard lock(m_queueLock); + RingBuffer& queue = !m_txImmQueue.isEmpty() ? m_txImmQueue : m_txQueue; + if (queue.isEmpty()) + return 0U; + + queue.peek(&storedLength, 1U); + + if (queue.dataSize() <= storedLength) + return 0U; + if (imm != nullptr) + *imm = !m_txImmQueue.isEmpty(); + + queue.get(&storedLength, 1U); + queue.get(data, storedLength); + shouldReset = (m_rfResetPending || m_netResetPending) && + m_txQueue.isEmpty() && m_txImmQueue.isEmpty(); + } + + if (shouldReset) { + if (m_rfResetPending) { + m_rfResetPending = false; + resetRF(); + } + if (m_netResetPending) { + m_netResetPending = false; + resetNet(); + } + } + + return storedLength; +} + +/* Processes one decoded network frame. */ + +bool Slot::processNetwork(uint8_t* data, uint32_t length, const lc::LC& control, + defines::P2_DUID::E duid, uint8_t controlByte) +{ + CHECK_NET_AUTHORITATIVE(control.getDstId()); + + if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { + ++m_netMissed; + if (m_debug) + LogDebugEx(LOG_NET, "Slot::processNetwork()", "P25 Phase 2 Slot %u, dropping network burst while RF owns slot, srcId = %u, dstId = %u", + m_slotNo + 1U, control.getSrcId(), control.getDstId()); + return false; + } + + if (m_netLastDstId != 0U && control.getDstId() != 0U && m_netState != RS_NET_IDLE && + m_netLastDstId != control.getDstId() && m_netTGHang.isRunning() && !m_netTGHang.hasExpired()) { + ++m_netMissed; + if (m_debug) + LogDebugEx(LOG_NET, "Slot::processNetwork()", "P25 Phase 2 Slot %u, dropping network burst during TG hang, lastDstId = %u, dstId = %u", + m_slotNo + 1U, m_netLastDstId, control.getDstId()); + return false; + } + + if (m_netLastDstId != 0U && m_netLastDstId == control.getDstId() && + m_netTGHang.isRunning() && !m_netTGHang.hasExpired()) + m_netTGHang.start(); + + m_control = control; + m_duid = duid; + m_controlByte = controlByte; + + // TODO(P25P2-FW): Replace this inferred offset with modem-reported + // superframe/ultraframe timing before enabling over-the-air operation. + m_netScrambleOffset = static_cast(((m_netBurstCount * 2U + m_slotNo) * P25DEF::P25_P2_BURST_LENGTH_BITS + + (P25DEF::P25_P2_BURST_LENGTH_BITS / 2U)) % P25DEF::P25_P2_SCRAMBLER_SEQUENCE_BITS); + m_control.setP2ScrambleOffset(m_netScrambleOffset); + + if (isLCCHDUID(duid)) { + // TODO(P25P2-LCCH): Network LCCH messages belong to a dedicated LCCH + // controller and must be scheduled only on an LCH designated LCCH. + return false; + } + if (!isVoiceDUID(duid) && !isFACCHDUID(duid) && !isSACCHDUID(duid)) + return false; + + const bool voice = isVoiceDUID(duid); + bool release = false; + if (voice) { + if (!m_voice.processNetwork(data, length)) + return false; + } else if (!processMAC(data, length, duid, true, release)) { + return false; + } + + m_networkWatchdog.start(); + + if (!voice) { + // handle MAC PDU opcodes for network voice calls + switch (m_control.getMACPDUOpcode()) { + case defines::P2_MAC_HEADER_OPCODE::PTT: + if (m_netVCHState == VCH_STATE::IDLE) { + m_netFrames = 0U; + m_netLost = 0U; + m_netMissed = 0U; + m_netBits = 1U; + m_netErrs = 0U; + + ::ActivityLog("P25P2", false, "Slot %u network voice call from %u to %u", m_slotNo + 1U, control.getSrcId(), control.getDstId()); + LogInfoEx(LOG_NET, "P25 Phase 2 voice call start, slot = %u, srcId = %u, dstId = %u", m_slotNo + 1U, control.getSrcId(), control.getDstId()); + } + + if (m_netVCHState == VCH_STATE::HANGTIME) { + m_netCallHangTimer.stop(); + m_netEndPTTCount = 0U; + m_netResetPending = false; + } + + m_netVCHState = VCH_STATE::PTT; + m_vcuElapsedMs = 0U; + break; + case defines::P2_MAC_HEADER_OPCODE::ACTIVE: + m_netVCHState = VCH_STATE::ACTIVE; + break; + case defines::P2_MAC_HEADER_OPCODE::HANGTIME: + m_netVCHState = VCH_STATE::HANGTIME; + m_netCallHangTimer.start(); + break; + case defines::P2_MAC_HEADER_OPCODE::END_PTT: + m_netEndPTTCount++; + if (m_netEndPTTCount == 2U) { + ::ActivityLog("P25P2", false, "Slot %u network voice call ended from %u to %u, %.1f seconds, packet loss: %u, BER: %.1f%%", + m_slotNo + 1U, m_netLastSrcId, m_netLastDstId, float(m_netFrames) / 16.667F, m_netLost, float(m_netErrs * 100U) / float(m_netBits)); + LogInfoEx(LOG_NET, "P25 Phase 2 voice call end, slot = %u, srcId = %u, dstId = %u, frames = %u, lost = %u, missed = %u, bits = %u, errors = %u, BER = %.4f%%", + m_slotNo + 1U, m_netLastSrcId, m_netLastDstId, m_netFrames, m_netLost, m_netMissed, m_netBits, m_netErrs, float(m_netErrs * 100U) / float(m_netBits)); + + m_netVCHState = VCH_STATE::TERMINATING; + m_netResetPending = true; + } + break; + default: + break; + } + } + + if (release) + writeEnd(true); + + m_netState = RS_NET_AUDIO; + m_netLastDstId = control.getDstId(); + m_netLastSrcId = control.getSrcId(); + + touchGrant(m_netLastDstId); + + if (!m_netTimeoutTimer.isRunning()) + m_netTimeoutTimer.start(); + + m_netTGHang.start(); + m_elapsedMs = 0U; + m_netBurstCount++; + return true; +} + +/* Advances slot timers. */ + +void Slot::clock(uint32_t ms) +{ + m_elapsedMs += ms; + m_vcuElapsedMs += ms; + + if (m_frameLossCnt > 0U && m_rfState == RS_RF_LISTENING) + m_frameLossCnt = 0U; + if (m_frameLossCnt > 0U && m_frameLossCnt >= m_frameLossThreshold && + (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA)) + processFrameLoss(RF_LOSS_TYPE_EXCEEDED_FRAME_THRESHOLD); + + // handle timeouts and hang timers + m_rfTimeoutTimer.clock(ms); + + if (m_rfTimeoutTimer.isRunning() && m_rfTimeoutTimer.hasExpired()) { + if (!m_rfTimeout) { + LogInfoEx(LOG_RF, "P25 Phase 2 Slot %u, traffic timeout timer has expired, traffic will not transmit", m_slotNo + 1U); + m_rfTimeout = true; + } + + m_rfTimeoutTimer.stop(); + m_rfTimeout = true; + + writeEnd(false); + } + + if (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA) { + if (m_rfLossWatchdog.isRunning()) { + m_rfLossWatchdog.clock(ms); + + if (m_rfLossWatchdog.hasExpired()) { + m_rfLossWatchdog.stop(); + + processFrameLoss(RF_LOSS_TYPE_LOSS_WATCHDOG); + } + } + } + + if (m_rfTGHang.isRunning()) { + m_rfTGHang.clock(ms); + + if (m_rfTGHang.hasExpired()) { + m_rfTGHang.stop(); + if (m_verbose) { + LogInfoEx(LOG_RF, "P25 Phase 2 Slot %u, talkgroup hang has expired, lastDstId = %u", m_slotNo, m_rfLastDstId); + } + m_rfLastDstId = 0U; + m_rfLastSrcId = 0U; + + // reset permitted ID and clear permission state + if (!s_authoritative && m_permittedDstId != 0U) { + m_permittedDstId = 0U; + } + + // has the talkgroup hang timer expired while the modem is in a non-listening state? + if (m_rfState != RS_RF_LISTENING) { + processFrameLoss(RF_LOSS_TYPE_TG_HANG_NOT_LISTENING); + } + else { + resetRF(); + } + } + } + + if (m_netTimeoutTimer.isRunning()) { + m_netTimeoutTimer.clock(ms); + if (m_netTimeoutTimer.hasExpired()) { + m_netTimeoutTimer.stop(); + if (!m_netTimeout) { + LogInfoEx(LOG_NET, "P25 Phase 2 Slot %u, traffic timeout timer has expired, traffic will not transmit", m_slotNo + 1U); + m_netTimeout = true; + } + + writeEnd(true); + } + } + + if (s_authoritative) { + if (m_netTGHang.isRunning()) { + m_netTGHang.clock(ms); + + if (m_netTGHang.hasExpired()) { + m_netTGHang.stop(); + if (m_verbose) { + LogInfoEx(LOG_NET, "P25 Phase 2 Slot %u, talkgroup hang has expired, lastDstId = %u", m_slotNo, m_netLastDstId); + } + m_netLastDstId = 0U; + m_netLastSrcId = 0U; + + resetNet(); + } + } + } + else { + m_netTGHang.stop(); + } + + if (m_netState == RS_NET_AUDIO || m_netState == RS_NET_DATA) { + m_networkWatchdog.clock(ms); + + if (m_networkWatchdog.hasExpired()) { + if (m_netState == RS_NET_AUDIO) { + // We've received the voice header haven't we? + ++m_netLost; + ::ActivityLog("P25P2", false, "Slot %u network watchdog has expired, %.1f seconds, %u%% packet loss, BER: %.1f%%", + m_slotNo + 1U, float(m_netFrames) / 16.667F, m_netFrames > 0U ? (m_netLost * 100U) / m_netFrames : 100U, float(m_netErrs * 100U) / float(m_netBits)); + writeEnd(true); + } + else { + ::ActivityLog("P25P2", false, "Slot %u network watchdog has expired", m_slotNo + 1U); + writeEnd(true); + } + } + } + + // During message-trunking hangtime, request one logical HANGTIME VCU per + // nominal 360 ms superframe. + // TODO(P25P2-FW): Drive this request from the modem's actual superframe + // boundary and place it only in the LCH's physical SACCH opportunity. + if (m_rfCallHangTimer.isRunning()) { + m_rfCallHangTimer.clock(ms); + if (m_elapsedMs >= 360U && m_rfVCHState == VCH_STATE::HANGTIME) { + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::HANGTIME); + m_elapsedMs %= 360U; + } + + if (m_rfCallHangTimer.hasExpired()) { + m_rfCallHangTimer.stop(); + writeEnd(false); + } + } + + if (m_netCallHangTimer.isRunning()) { + m_netCallHangTimer.clock(ms); + if (m_elapsedMs >= 360U && m_netVCHState == VCH_STATE::HANGTIME) { + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::HANGTIME); + m_elapsedMs %= 360U; + } + + if (m_netCallHangTimer.hasExpired()) { + m_netCallHangTimer.stop(); + writeEnd(true); + } + } + + // P2Voice owns VCU construction; the slot currently supplies policy cadence. + // TODO(P25P2-FW): The modem scheduler must select the legal SACCH slot + // and report the fourth-superframe talker SACCH opportunity to the host. + if (m_vcuElapsedMs >= 360U && (m_rfVCHState == VCH_STATE::ACTIVE || m_netVCHState == VCH_STATE::ACTIVE)) { + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::ACTIVE); + m_vcuElapsedMs %= 360U; + } +} + +/* Helper to initialize shared P25 Phase 2 slot configuration. */ + +void Slot::init(Control* control, bool authoritative, uint32_t callHang, modem::Modem* modem, network::Network* network, + lookups::P25AffiliationLookup* affiliations, ::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup) +{ + s_control = control; + + s_authoritative = authoritative; + + s_callHang = callHang; + + s_modem = modem; + s_network = network; + + s_affiliations = affiliations; + s_ridLookup = ridLookup; + s_tidLookup = tidLookup; + + if (s_ridLookup != nullptr && s_tidLookup != nullptr) + acl::AccessControl::init(s_ridLookup, s_tidLookup); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Adds a frame to the slot queue. */ + +bool Slot::addFrame(const uint8_t* data, uint32_t length, defines::P2_DUID::E duid, + bool net, bool imm) +{ + if (data == nullptr || length != P25_P2_FRAME_LENGTH_BYTES) + return false; + + std::lock_guard lock(m_queueLock); + + if (!net) { + if (m_rfTimeoutTimer.isRunning() && m_rfTimeoutTimer.hasExpired()) + return false; + } else { + if (m_netTimeoutTimer.isRunning() && m_netTimeoutTimer.hasExpired()) + return false; + } + + uint8_t len = P25_P2_HOST_FRAME_LENGTH_BYTES; + if (m_debug) { + Utils::symbols("!!! *Tx P25P2", data, len); + } + + uint32_t fifoSpace = 0U; + if (s_modem != nullptr) + fifoSpace = m_slotNo == 0U ? s_modem->getP25P2Space1() : s_modem->getP25P2Space2(); + + uint8_t tag = modem::TAG_DATA; + uint8_t duidValue = static_cast(duid); + + // is this immediate data? + if (imm) { + // resize immediate queue if necessary (this shouldn't really ever happen) + uint32_t space = m_txImmQueue.freeSpace(); + if (space < (len + 1U)) { + if (!net) { + uint32_t queueLen = m_txImmQueue.length(); + m_txImmQueue.resize(queueLen + len); + LogError(LOG_P25, "Slot %u, overflow in the imm Phase 2 slot queue; queue free is %u, needed %u; resized was %u is %u, fifoSpace = %u", m_slotNo, space, len, queueLen, m_txQueue.length(), fifoSpace); + return false; + } + else { + LogError(LOG_P25, "Slot %u, overflow in the imm Phase 2 slot queue while writing network data; queue free is %u, needed %u, fifoSpace = %u", m_slotNo, space, len, fifoSpace); + return false; + } + } + + m_txImmQueue.addData(&len, 1U); + m_txImmQueue.addData(&tag, 1U); + m_txImmQueue.addData(&duidValue, 1U); + m_txImmQueue.addData(data, length); + return true; + } + + uint32_t space = m_txQueue.freeSpace(); + if (space < (len + 1U)) { + if (!net) { + uint32_t queueLen = m_txQueue.length(); + m_txQueue.resize(queueLen + (len + 2U)); + LogError(LOG_P25, "Slot %u, overflow in the Phase 2 slot queue; queue free is %u, needed %u; resized was %u is %u, fifoSpace = %u", m_slotNo, space, len, queueLen, m_txQueue.length(), fifoSpace); + return false; + } + else { + LogError(LOG_P25, "Slot %u, overflow in the Phase 2 slot queue while writing network data; queue free is %u, needed %u, fifoSpace = %u", m_slotNo, space, len, fifoSpace); + return false; + } + } + + m_txQueue.addData(&len, 1U); + m_txQueue.addData(&tag, 1U); + m_txQueue.addData(&duidValue, 1U); + m_txQueue.addData(data, length); + + return true; +} + +/* Resets RF state after a timeout or talkgroup hang. */ + +void Slot::resetRF() +{ + if (s_affiliations != nullptr && m_rfLastDstId != 0U && s_affiliations->isGranted(m_rfLastDstId)) + s_affiliations->releaseGrant(m_rfLastDstId, false); + + m_rfState = RS_RF_LISTENING; + m_rfLastDstId = 0U; + m_rfLastSrcId = 0U; + + m_rfTimeoutTimer.stop(); + m_rfTGHang.stop(); + m_rfLossWatchdog.stop(); + m_rfCallHangTimer.stop(); + + m_rfVCHState = VCH_STATE::IDLE; + + m_rfPTTCount = 0U; + m_rfEndPTTCount = 0U; + m_frameLossCnt = 0U; + m_rfTimeout = false; + + m_rfFrames = 0U; + m_rfBits = 1U; + m_rfErrs = 0U; + + m_vcuElapsedMs = 0U; + + m_voice.resetRF(); + m_rfResetPending = false; +} + +/* Resets network state after a timeout, hang, or watchdog expiration. */ + +void Slot::resetNet() +{ + if (s_affiliations != nullptr && m_netLastDstId != 0U) + s_affiliations->releaseGrant(m_netLastDstId, false); + if (s_control != nullptr && s_control->m_network != nullptr) + s_control->m_network->resetP25P2(m_slotNo); + + m_netState = RS_NET_IDLE; + m_netLastDstId = 0U; + m_netLastSrcId = 0U; + + m_netTimeoutTimer.stop(); + m_netTGHang.stop(); + m_networkWatchdog.stop(); + m_netCallHangTimer.stop(); + + m_netVCHState = VCH_STATE::IDLE; + m_netEndPTTCount = 0U; + m_netTimeout = false; + + m_netFrames = 0U; + m_netLost = 0U; + m_netMissed = 0U; + m_netBits = 1U; + m_netErrs = 0U; + + m_vcuElapsedMs = 0U; + + m_voice.resetNet(); + m_netResetPending = false; +} + +/* Permits a destination on a non-authoritative host. */ + +void Slot::permittedTG(uint32_t dstId) +{ + if (s_authoritative) + return; + + if (m_verbose) { + if (dstId == 0U) + LogInfoEx(LOG_P25, "P25 Phase 2 Slot %u, non-authoritative TG unpermit", m_slotNo + 1U); + else + LogInfoEx(LOG_P25, "P25 Phase 2 Slot %u, non-authoritative TG permit, dstId = %u", + m_slotNo + 1U, dstId); + } + + if (m_permittedDstId != 0U && m_permittedDstId != dstId) { + if (m_netState != RS_NET_IDLE && m_netLastDstId == m_permittedDstId) + writeEnd(true); + if (m_rfState != RS_RF_LISTENING && m_rfLastDstId == m_permittedDstId) + writeEnd(false); + } + m_permittedDstId = dstId; +} + +/* Touches a granted destination at the request of the CC. */ + +void Slot::touchGrantTG(uint32_t dstId) +{ + touchGrant(dstId); +} + +/* Releases a granted destination at the request of the CC. */ + +void Slot::releaseGrantTG(uint32_t dstId) +{ + if (dstId == 0U || s_affiliations == nullptr || !s_affiliations->isGranted(dstId)) + return; + + if (m_verbose) + LogInfoEx(LOG_P25, "P25 Phase 2 Slot %u, releasing grant, dstId = %u", + m_slotNo + 1U, dstId); + s_affiliations->releaseGrant(dstId, false); +} + +/* Touches a grant associated with accepted traffic. */ + +void Slot::touchGrant(uint32_t dstId) +{ + if (dstId == 0U || s_affiliations == nullptr || !s_affiliations->isGranted(dstId)) + return; + + if (m_verbose) + LogInfoEx(LOG_P25, "P25 Phase 2 Slot %u, call in progress, touching grant, dstId = %u", + m_slotNo + 1U, dstId); + s_affiliations->touchGrant(dstId); +} + +/* Clears a rejected RF state. */ + +void Slot::clearRFReject() +{ + if (m_rfState != RS_RF_REJECTED) + return; + + { + std::lock_guard lock(m_queueLock); + m_txQueue.clear(); + m_txImmQueue.clear(); + } + resetRF(); +} + +/* Validates RF call ownership and access-control policy. */ + +bool Slot::validateRFCall(uint32_t srcId, uint32_t dstId, bool group) +{ + if (!s_authoritative && m_permittedDstId != dstId) { + LogWarning(LOG_RF, + "P25 Phase 2 Slot %u, [NON-AUTHORITATIVE] destination not permitted, dstId = %u", + m_slotNo + 1U, dstId); + { + std::lock_guard lock(m_queueLock); + m_txQueue.clear(); + m_txImmQueue.clear(); + } + m_rfState = RS_RF_LISTENING; + return false; + } + + if (m_netState != RS_NET_IDLE) { + LogWarning(LOG_RF, + "P25 Phase 2 Slot %u, RF traffic collision with active network call, srcId = %u, dstId = %u", + m_slotNo + 1U, srcId, dstId); + m_rfState = RS_RF_LISTENING; + return false; + } + + const bool aclEnabled = s_ridLookup != nullptr && s_tidLookup != nullptr; + const bool validSrc = !aclEnabled || acl::AccessControl::validateSrcId(srcId); + const bool validDst = !aclEnabled || (group ? acl::AccessControl::validateTGId(dstId) : + acl::AccessControl::validateSrcId(dstId)); + if (validSrc && validDst) + return true; + + LogWarning(LOG_RF, "P25 Phase 2 Slot %u, RF voice rejection, srcId = %u, dstId = %u", + m_slotNo + 1U, srcId, dstId); + ::ActivityLog("P25P2", true, "Slot %u RF voice rejection from %u to %u", + m_slotNo + 1U, srcId, dstId); + m_rfLastDstId = 0U; + m_rfLastSrcId = 0U; + m_rfTGHang.stop(); + m_rfLossWatchdog.stop(); + { + std::lock_guard lock(m_queueLock); + m_txQueue.clear(); + m_txImmQueue.clear(); + } + m_rfState = RS_RF_REJECTED; + return false; +} + +/* Helper to process loss of frame stream from modem. */ + +void Slot::processFrameLoss(RPT_RF_LOSS_TYPE type) +{ + // resolve the type of RF frame loss into a human-readable string + std::string typeStr; + switch (type) { + case RF_LOSS_TYPE_EXCEEDED_FRAME_THRESHOLD: + typeStr = "exceeded frame loss threshold"; + break; + case RF_LOSS_TYPE_LOSS_WATCHDOG: + typeStr = "loss watchdog timeout"; + break; + case RF_LOSS_TYPE_IN_CALL_CONTROL: + typeStr = "in-call control request"; + break; + case RF_LOSS_TYPE_TG_HANG_NOT_LISTENING: + typeStr = "TG hang, RF not listening"; + break; + default: + typeStr = "no loss type set BUGBUG"; + break; + } + + m_rfLossWatchdog.stop(); + if (m_rfState == RS_RF_AUDIO) { + ::ActivityLog("P25P2", true, "Slot %u RF voice transmission lost, %s, %.1f seconds, BER: %.1f%%, loss count: %u", + m_slotNo + 1U, typeStr.c_str(), float(m_rfFrames) / 16.667F, float(m_rfErrs * 100U) / float(m_rfBits), m_frameLossCnt); + + LogInfoEx(LOG_RF, "P25 Phase 2 Slot %u, transmission lost, %s, total frames: %d, total bits: %d, errors: %d, BER: %.4f%%", + m_slotNo + 1U, typeStr.c_str(), m_rfFrames, m_rfBits, m_rfErrs, float(m_rfErrs * 100U) / float(m_rfBits)); + + writeEnd(false); + } else { + resetRF(); + } + + m_rfState = RS_RF_LISTENING; + + m_rfLastDstId = 0U; + m_rfTGHang.stop(); + m_rfLossWatchdog.stop(); + + m_txQueue.clear(); +} + +/* Enters message-trunking hangtime. */ + +void Slot::beginHangtime(bool net) +{ + if (net) + m_netVCHState = VCH_STATE::HANGTIME; + else + m_rfVCHState = VCH_STATE::HANGTIME; + + m_elapsedMs = 0U; + + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::HANGTIME, true); + + if (s_callHang > 0U) { + if (net) + m_netCallHangTimer.start(); + else + m_rfCallHangTimer.start(); + } + else + writeEnd(net); +} + +/* Queues the two-burst FNE termination sequence described by TIA-102.BBAE 4.5.3. */ + +void Slot::writeEnd(bool net) +{ + VCH_STATE& state = net ? m_netVCHState : m_rfVCHState; + if (state == VCH_STATE::TERMINATING) + return; + + state = VCH_STATE::TERMINATING; + if (net) + m_netCallHangTimer.stop(); + else + m_rfCallHangTimer.stop(); + if (net) { + m_networkWatchdog.stop(); + m_netTimeoutTimer.stop(); + m_netTimeout = false; + } else { + m_rfLossWatchdog.stop(); + m_rfTimeoutTimer.stop(); + + m_rfTimeout = false; + } + + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::END_PTT, true); + queueMACPDU(defines::P2_MAC_HEADER_OPCODE::END_PTT, true); + if (net) + m_netResetPending = true; + else + m_rfResetPending = true; +} + +/* Encodes one outbound logical FACCH PDU; physical slot pumping remains modem-owned. */ + +bool Slot::queueMACPDU(uint8_t opcode, bool imm) +{ + if (opcode == defines::P2_MAC_HEADER_OPCODE::ACTIVE || + opcode == defines::P2_MAC_HEADER_OPCODE::HANGTIME) + return m_voice.writeVoiceLC(opcode, imm); + + lc::LC control(m_control); + control.setMACPDUOpcode(opcode); + control.setP2DUID(static_cast(defines::P2_DUID::FACCH_UNSCRAMBLED)); + control.setP2ScrambleOffset(m_netScrambleOffset); + + if (opcode == defines::P2_MAC_HEADER_OPCODE::END_PTT) + control.setSrcId(0xFFFFFFU); + + uint8_t burst[P25DEF::P25_P2_FRAME_LENGTH_BYTES] = { 0U }; + + // Outbound FACCH uses the sync-bearing S-OEMI structure. The immediate + // queue means "next legal FACCH opportunity", not the next physical slot. + // TODO(P25P2-FW): Enforce that priority interpretation in modem. + control.encodeVCH_MACPDU(burst, true); + + return addFrame(burst, sizeof(burst), defines::P2_DUID::FACCH_UNSCRAMBLED, true, imm); +} + +/* Processes and forwards one logical MAC-bearing burst. */ + +bool Slot::processMAC(uint8_t* data, uint32_t length, defines::P2_DUID::E duid, + bool net, bool& release) +{ + release = false; + if (data == nullptr || length != P25DEF::P25_P2_FRAME_LENGTH_BYTES || + isVoiceDUID(duid) || isLCCHDUID(duid) || (!isFACCHDUID(duid) && !isSACCHDUID(duid))) + return false; + + uint8_t frame[P25DEF::P25_P2_FRAME_LENGTH_BYTES]; + ::memcpy(frame, data, sizeof(frame)); + + // RF MAC bursts were decoded during DUID dispatch. Network bursts carry + // outbound encoding and must be decoded before message classification. + // Outbound FACCH is S-OEMI (sync); outbound SACCH is I-OEMI (no sync). + const bool sync = isFACCHDUID(duid); + if (net && !m_control.decodeVCH_MACPDU_OEMI(frame, sync)) + return false; + + // The transport discriminator and the discriminator protected inside the + // MAC burst must agree. Do not let metadata reinterpret one coded form + // as another after the modem has classified the logical channel. + if (static_cast(m_control.getP2DUID() & 0x0FU) != duid) + return false; + + m_control.setP2DUID(static_cast(duid)); + std::unique_ptr mac; + const uint8_t headerOpcode = m_control.getMACPDUOpcode(); + if (!net && + (headerOpcode == defines::P2_MAC_HEADER_OPCODE::PTT || + (headerOpcode == defines::P2_MAC_HEADER_OPCODE::ACTIVE && m_rfState == RS_RF_LATE_ENTRY))) + CHECK_AUTHORITATIVE(m_control.getSrcId(), m_control.getDstId(), + m_control.getLCO() != defines::P2_MAC_MCO::PRIVATE); + + if (headerOpcode == defines::P2_MAC_HEADER_OPCODE::IDLE || + headerOpcode == defines::P2_MAC_HEADER_OPCODE::ACTIVE || + headerOpcode == defines::P2_MAC_HEADER_OPCODE::HANGTIME) { + mac = lc::mac::MACFactory::createMACPDU(m_control); + if (mac == nullptr && m_control.getLCO() != defines::P2_MAC_MCO::PDU_NULL) + return false; + } + + if (m_debug) + LogDebugEx(LOG_P25, "Slot::processMAC()", "slot = %u, opcode = $%02X, MCO = $%02X, duid = $%02X", m_slotNo, + headerOpcode, m_control.getLCO(), static_cast(duid)); + + if (mac != nullptr) { + switch (mac->getOpcode()) { + case defines::P2_MAC_MCO::MAC_RELEASE: + if (m_verbose) + LogInfoEx(net ? LOG_NET : LOG_RF, "P25 Phase 2 Slot %u, MAC release, srcId = %u, dstId = %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + release = true; + break; + case defines::P2_MAC_MCO::GROUP: + case defines::P2_MAC_MCO::PRIVATE: + case defines::P2_MAC_MCO::TEL_INT_VCH_USER: + if (m_verbose) + LogInfoEx(net ? LOG_NET : LOG_RF, "P25 Phase 2 Slot %u, MAC voice user, srcId = %u, dstId = %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + break; + default: + break; + } + } + + // Normalize the MAC PDU into the outbound logical form expected by the + // modem's slot pump. The modem supplies the physical FACCH/SACCH timing. + m_control.encodeVCH_MACPDU(frame, sync); + if (net) + return !m_netTimeout && addFrame(frame, sizeof(frame), duid, true); + + if (m_rfTimeout || !addFrame(frame, sizeof(frame), duid)) + return false; + if (s_control != nullptr) + s_control->writeNetwork(this, frame, sizeof(frame)); + return true; +} + +/* Returns whether a DUID identifies a voice traffic burst. */ + +bool Slot::isVoiceDUID(defines::P2_DUID::E duid) +{ + return duid == defines::P2_DUID::VTCH_4V || duid == defines::P2_DUID::VTCH_2V; +} + +/* Returns whether a DUID identifies a FACCH burst. */ + +bool Slot::isFACCHDUID(defines::P2_DUID::E duid) +{ + return duid == defines::P2_DUID::FACCH_SCRAMBLED || duid == defines::P2_DUID::FACCH_UNSCRAMBLED; +} + +/* Returns whether a DUID identifies a SACCH burst. */ + +bool Slot::isSACCHDUID(defines::P2_DUID::E duid) +{ + return duid == defines::P2_DUID::SACCH_SCRAMBLED || duid == defines::P2_DUID::SACCH_UNSCRAMBLED; +} + +/* Returns whether a DUID identifies an LCCH burst. */ + +bool Slot::isLCCHDUID(defines::P2_DUID::E duid) +{ + return duid == defines::P2_DUID::LCCH_SCRAMBLED || duid == defines::P2_DUID::LCCH_UNSCRAMBLED; +} diff --git a/src/host/p25/phase2/Slot.h b/src/host/p25/phase2/Slot.h new file mode 100644 index 00000000..d0afd0c4 --- /dev/null +++ b/src/host/p25/phase2/Slot.h @@ -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 +#include + +// --------------------------------------------------------------------------- +// 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 m_txQueue; + RingBuffer 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__ diff --git a/tests/edac/AMBE2FEC_Tests.cpp b/tests/edac/AMBE2FEC_Tests.cpp new file mode 100644 index 00000000..a2c08e28 --- /dev/null +++ b/tests/edac/AMBE2FEC_Tests.cpp @@ -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 + +#include + +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(((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(((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(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(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); +} \ No newline at end of file diff --git a/tests/edac/AMBEFEC_Tests.cpp b/tests/edac/AMBEFEC_Tests.cpp new file mode 100644 index 00000000..7d55c62d --- /dev/null +++ b/tests/edac/AMBEFEC_Tests.cpp @@ -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 + +#include +#include + +using namespace edac; + +TEST_CASE("AMBEFEC regenerates a clean P25 IMBE frame", "[edac][ambefec][p25]") +{ + AMBEFEC fec; + std::array 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 clean{}; + fec.regenerateIMBE(clean.data()); + REQUIRE(fec.regenerateIMBE(clean.data()) == 0U); + + for (uint32_t bit = 0U; bit < 144U; bit += 17U) { + std::array frame = clean; + frame[bit >> 3U] ^= static_cast(1U << (7U - (bit & 7U))); + fec.regenerateIMBE(frame.data()); + REQUIRE(std::memcmp(frame.data(), clean.data(), frame.size()) == 0); + } +} \ No newline at end of file diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index e642467e..1dafd8ec 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -163,6 +163,120 @@ bool HostTestHooks::dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_ return slot.processFrame(frame, sizeof(frame)); } +/* Gets a P25 Phase 2 slot for tests. */ + +p25::phase2::Slot& HostTestHooks::p25P2Slot(p25::phase2::Control& control, uint32_t slotNo) +{ + assert(slotNo < p25::phase2::Control::SLOT_COUNT); + return slotNo == 0U ? *control.m_slot1 : *control.m_slot2; +} + +/* Gets P25 Phase 2 RF state. */ + +RPT_RF_STATE HostTestHooks::p25P2RFState(const p25::phase2::Slot& slot) { return slot.m_rfState; } + +void HostTestHooks::p25P2SetRFState(p25::phase2::Slot& slot, RPT_RF_STATE state) { slot.m_rfState = state; } + +/* Gets P25 Phase 2 last source ID. */ + +uint32_t HostTestHooks::p25P2LastSrcId(const p25::phase2::Slot& slot) +{ + return slot.m_rfLastSrcId != 0U ? slot.m_rfLastSrcId : slot.m_netLastSrcId; +} + +/* Gets P25 Phase 2 last destination ID. */ + +uint32_t HostTestHooks::p25P2LastDstId(const p25::phase2::Slot& slot) +{ + return slot.m_rfLastDstId != 0U ? slot.m_rfLastDstId : slot.m_netLastDstId; +} + +/* Gets P25 Phase 2 RF scramble offset. */ + +uint16_t HostTestHooks::p25P2RFScrambleOffset(const p25::phase2::Slot& slot) { return slot.m_rfScrambleOffset; } + +/* Gets P25 Phase 2 network scramble offset. */ + +uint16_t HostTestHooks::p25P2NetScrambleOffset(const p25::phase2::Slot& slot) { return slot.m_netScrambleOffset; } + +/* Gets whether P25 Phase 2 ESS is complete. */ + +bool HostTestHooks::p25P2ESSComplete(const p25::phase2::Slot& slot) +{ + return slot.m_voice.m_rfESSComplete || slot.m_voice.m_netESSComplete; +} + +/* Gets P25 Phase 2 ESS algorithm ID. */ + +uint8_t HostTestHooks::p25P2ESSAlgId(const p25::phase2::Slot& slot) +{ + const uint8_t* ess = slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS : slot.m_voice.m_rfESS; + return ess[0U]; +} + +/* Gets P25 Phase 2 ESS key ID. */ + +uint16_t HostTestHooks::p25P2ESSKeyId(const p25::phase2::Slot& slot) +{ + const uint8_t* ess = slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS : slot.m_voice.m_rfESS; + return static_cast((ess[1U] << 8U) | ess[2U]); +} + +/* Copies P25 Phase 2 ESS MI. */ + +void HostTestHooks::p25P2ESSMI(const p25::phase2::Slot& slot, uint8_t* mi) +{ + if (mi != nullptr) + ::memcpy(mi, slot.m_voice.m_netESSComplete ? slot.m_voice.m_netESS + 3U : + slot.m_voice.m_rfESS + 3U, 9U); +} + +/* Gets P25 Phase 2 RF VCH state. */ + +p25::phase2::Slot::VCH_STATE HostTestHooks::p25P2RFVCHState(const p25::phase2::Slot& slot) { return slot.m_rfVCHState; } + +/* Gets P25 Phase 2 network VCH state. */ + +p25::phase2::Slot::VCH_STATE HostTestHooks::p25P2NetVCHState(const p25::phase2::Slot& slot) { return slot.m_netVCHState; } + +/* Gets P25 Phase 2 DUID. */ + +p25::defines::P2_DUID::E HostTestHooks::p25P2DUID(const p25::phase2::Slot& slot) { return slot.m_duid; } + +/* Gets P25 Phase 2 RF PTT count. */ + +uint8_t HostTestHooks::p25P2RFPTTCount(const p25::phase2::Slot& slot) { return slot.m_rfPTTCount; } + +/* Gets P25 Phase 2 RF frames count. */ + +uint32_t HostTestHooks::p25P2RFFrames(const p25::phase2::Slot& slot) { return slot.m_rfFrames; } + +/* Gets P25 Phase 2 network frames count. */ + +uint32_t HostTestHooks::p25P2NetFrames(const p25::phase2::Slot& slot) { return slot.m_netFrames; } + +/* Gets P25 Phase 2 network lost frames count. */ +uint32_t HostTestHooks::p25P2NetLost(const p25::phase2::Slot& slot) { return slot.m_netLost; } + +/* Gets P25 Phase 2 network missed frames count. */ +uint32_t HostTestHooks::p25P2NetMissed(const p25::phase2::Slot& slot) { return slot.m_netMissed; } + +/* Gets P25 Phase 2 RF bits, network bits, RF errors, and network errors. */ + +uint32_t HostTestHooks::p25P2RFBits(const p25::phase2::Slot& slot) { return slot.m_rfBits; } + +/* Gets P25 Phase 2 network bits. */ + +uint32_t HostTestHooks::p25P2NetBits(const p25::phase2::Slot& slot) { return slot.m_netBits; } + +/* Gets P25 Phase 2 RF errors. */ + +uint32_t HostTestHooks::p25P2RFErrs(const p25::phase2::Slot& slot) { return slot.m_rfErrs; } + +/* Gets P25 Phase 2 network errors. */ + +uint32_t HostTestHooks::p25P2NetErrs(const p25::phase2::Slot& slot) { return slot.m_netErrs; } + /* Gets P25 network state. */ RPT_NET_STATE HostTestHooks::p25NetState(const p25::Control& control) { return control.m_netState; } diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index f5128cc8..51bbe991 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -14,6 +14,8 @@ #include "host/dmr/Slot.h" #include "host/nxdn/Control.h" #include "host/p25/Control.h" +#include "host/p25/phase2/Control.h" +#include "host/p25/phase2/Slot.h" #include "host/p25/packet/Voice.h" // --------------------------------------------------------------------------- @@ -217,6 +219,147 @@ public: * @param duid DUID to encode into frame NID bits. */ static void p25StampRFFrameNID(p25::Control& control, uint8_t* frame, p25::defines::DUID::E duid); + + /** + * @brief Gets a P25 Phase 2 slot for tests. + * @param control P25 Phase 2 control instance. + * @param slotNo Slot number to retrieve. + * @return p25::phase2::Slot& Reference to the requested Phase 2 slot. + */ + static p25::phase2::Slot& p25P2Slot(p25::phase2::Control& control, uint32_t slotNo); + /** + * @brief Gets P25 Phase 2 RF state. + * @param slot P25 Phase 2 slot instance. + * @return RPT_RF_STATE Current RF state of the slot. + */ + static RPT_RF_STATE p25P2RFState(const p25::phase2::Slot& slot); + /** + * @brief Forces P25 Phase 2 RF state for recovery-path tests. + * @param slot P25 Phase 2 slot instance. + * @param state RF state to set for the slot. + */ + static void p25P2SetRFState(p25::phase2::Slot& slot, RPT_RF_STATE state); + /** + * @brief Gets P25 Phase 2 last source ID. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Last source ID of the slot. + */ + static uint32_t p25P2LastSrcId(const p25::phase2::Slot& slot); + /** + * @brief Gets P25 Phase 2 last destination ID. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Last destination ID of the slot. + */ + static uint32_t p25P2LastDstId(const p25::phase2::Slot& slot); + /** + * @brief Gets P25 Phase 2 RF scramble offset. + * @param slot P25 Phase 2 slot instance. + * @return uint16_t RF scramble offset of the slot. + */ + static uint16_t p25P2RFScrambleOffset(const p25::phase2::Slot& slot); + /** + * @brief Gets P25 Phase 2 network scramble offset. + * @param slot P25 Phase 2 slot instance. + * @return uint16_t Network scramble offset of the slot. + */ + static uint16_t p25P2NetScrambleOffset(const p25::phase2::Slot& slot); + /** + * @brief Gets whether P25 Phase 2 ESS is complete. + * @param slot P25 Phase 2 slot instance. + * @return bool True if ESS is complete, false otherwise. + */ + static bool p25P2ESSComplete(const p25::phase2::Slot& slot); + /** + * @brief Gets P25 Phase 2 ESS algorithm ID. + * @param slot P25 Phase 2 slot instance. + * @return uint8_t ESS algorithm ID of the slot. + */ + static uint8_t p25P2ESSAlgId(const p25::phase2::Slot& slot); + /** + * @brief Gets P25 Phase 2 ESS key ID. + * @param slot P25 Phase 2 slot instance. + * @return uint16_t ESS key ID of the slot. + */ + static uint16_t p25P2ESSKeyId(const p25::phase2::Slot& slot); + /** + * @brief Copies P25 Phase 2 ESS MI. + * @param slot P25 Phase 2 slot instance. + * @param mi Buffer to copy the ESS MI into. + */ + static void p25P2ESSMI(const p25::phase2::Slot& slot, uint8_t* mi); + /** + * @brief Gets the RF VCH lifecycle state. + * @param slot P25 Phase 2 slot instance. + * @return p25::phase2::Slot::VCH_STATE RF VCH lifecycle state of the slot. + */ + static p25::phase2::Slot::VCH_STATE p25P2RFVCHState(const p25::phase2::Slot& slot); + /** + * @brief Gets the network VCH lifecycle state. + * @param slot P25 Phase 2 slot instance. + * @return p25::phase2::Slot::VCH_STATE Network VCH lifecycle state of the slot. + */ + static p25::phase2::Slot::VCH_STATE p25P2NetVCHState(const p25::phase2::Slot& slot); + /** + * @brief Gets the decoded Phase 2 DUID. + * @param slot P25 Phase 2 slot instance. + * @return p25::defines::P2_DUID::E Decoded Phase 2 DUID of the slot. + */ + static p25::defines::P2_DUID::E p25P2DUID(const p25::phase2::Slot& slot); + /** + * @brief Gets the decoded inbound FACCH PTT count. + * @param slot P25 Phase 2 slot instance. + * @return uint8_t Decoded inbound FACCH PTT count of the slot. + */ + static uint8_t p25P2RFPTTCount(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of RF frames received. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of RF frames received by the slot. + */ + static uint32_t p25P2RFFrames(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of network frames received. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of network frames received by the slot. + */ + static uint32_t p25P2NetFrames(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of network frames lost. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of network frames lost by the slot. + */ + static uint32_t p25P2NetLost(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of network frames missed. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of network frames missed by the slot. + */ + static uint32_t p25P2NetMissed(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of RF bits received. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of RF bits received by the slot. + */ + static uint32_t p25P2RFBits(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of network bits received. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of network bits received by the slot. + */ + static uint32_t p25P2NetBits(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of RF errors. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of RF errors of the slot. + */ + static uint32_t p25P2RFErrs(const p25::phase2::Slot& slot); + /** + * @brief Gets the number of network errors. + * @param slot P25 Phase 2 slot instance. + * @return uint32_t Number of network errors of the slot. + */ + static uint32_t p25P2NetErrs(const p25::phase2::Slot& slot); + /** * @brief Injects a synthetic P25 network call start via voice packet path. * @param control P25 control instance. diff --git a/tests/p25/MACPDU_Tests.cpp b/tests/p25/MACPDU_Tests.cpp new file mode 100644 index 00000000..22a126cc --- /dev/null +++ b/tests/p25/MACPDU_Tests.cpp @@ -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 + +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 mac = MACFactory::createMACPDU(control); + REQUIRE(mac != nullptr); + REQUIRE(mac->getOpcode() == defines::P2_MAC_MCO::GROUP); + + control.setLCO(0x7FU); + REQUIRE(MACFactory::createMACPDU(control) == nullptr); +} diff --git a/tests/p25/P2_Control_Tests.cpp b/tests/p25/P2_Control_Tests.cpp new file mode 100644 index 00000000..2d15dc8f --- /dev/null +++ b/tests/p25/P2_Control_Tests.cpp @@ -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 + +#include +#include +#include + +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 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(duid)); + control.setMACPDUOpcode(opcode); + + std::array frame{}; + frame[0U] = modem::TAG_DATA; + frame[1U] = static_cast(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 makeVoiceBurst( + P25DEF::P2_DUID::E duid, const std::array& ess, uint32_t essBNo) +{ + std::array frame{}; + frame[0U] = modem::TAG_DATA; + frame[1U] = static_cast(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& frame) +{ + p25::lc::LC decoded; + const auto duid = static_cast(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(length); + } + + /** + * @brief Closes the loopback port. + */ + void close() override { } + + std::vector> 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 slot0Frame{}; + std::array 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 privatePTT{}; + privatePTT[0U] = modem::TAG_DATA; + privatePTT[1U] = P25DEF::P2_DUID::SACCH_UNSCRAMBLED; + privateCall.encodeVCH_MACPDU_IEMI(privatePTT.data() + 2U, false); + + std::array 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 input{}; + input[0U] = modem::TAG_DATA; + input[1U] = P25DEF::P2_DUID::VTCH_4V; + input[2U] = static_cast(0x20U + slotNo); + + REQUIRE(transmitter.processFrame(slotNo, input.data(), input.size())); + + std::array 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& 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 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 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 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 ess{}; + ess[0U] = 0x80U; + ess[1U] = 0x12U; + ess[2U] = 0x34U; + for (uint32_t i = 0U; i < 9U; ++i) + ess[3U + i] = static_cast(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 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 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 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 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); +} diff --git a/tests/p25/P2_ESS_Tests.cpp b/tests/p25/P2_ESS_Tests.cpp new file mode 100644 index 00000000..ba52ad4c --- /dev/null +++ b/tests/p25/P2_ESS_Tests.cpp @@ -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 + +#include +#include + +using namespace p25::phase2; + +TEST_CASE("P25 Phase 2 ESS reconstructs ALGID KID and MI", "[p2][ess]") +{ + edac::RS634717 rs; + std::array codeword{}; + codeword[0U] = 0x80U; + codeword[1U] = 0x12U; + codeword[2U] = 0x34U; + for (uint32_t i = 0U; i < 9U; i++) codeword[3U + i] = static_cast(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 macBurst{}; + mac.encodeVCH_MACPDU(macBurst.data(), true); + + for (uint32_t burstNo = 0U; burstNo < 4U; burstNo++) { + std::array 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 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); +} diff --git a/tests/p25/P2_VCH_MACPDU_Test.cpp b/tests/p25/P2_VCH_MACPDU_Test.cpp index 6f6f3c6a..4578dbd3 100644 --- a/tests/p25/P2_VCH_MACPDU_Test.cpp +++ b/tests/p25/P2_VCH_MACPDU_Test.cpp @@ -24,19 +24,31 @@ using namespace p25::lc; #include #include +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- namespace { - void configureP2ScrambleContext(LC& lc, uint16_t scrambleOffset, uint16_t colorCode = DEFAULT_NAC) - { - static bool siteConfigured = false; - if (!siteConfigured) { - LC::setSiteData(SiteData(0xABCDEU, 0x123U, 1U, 1U, 1U, 1U, 1U, - ServiceClass::VOICE | ServiceClass::DATA, 0)); - siteConfigured = true; - } - lc.setColorCode(colorCode); - lc.setP2ScrambleOffset(scrambleOffset); +/** + * @brief Configures the LC instance with a P25 Phase 2 scramble context. + * @param lc LC instance to configure. + * @param scrambleOffset Scramble offset to set. + * @param colorCode Color code to set (default is DEFAULT_NAC). + * @note This function sets the site data for the LC instance if it has not been + */ +void configureP2ScrambleContext(LC& lc, uint16_t scrambleOffset, uint16_t colorCode = DEFAULT_NAC) +{ + static bool siteConfigured = false; + if (!siteConfigured) { + LC::setSiteData(SiteData(0xABCDEU, 0x123U, 1U, 1U, 1U, 1U, 1U, + ServiceClass::VOICE | ServiceClass::DATA, 0)); + siteConfigured = true; } + + lc.setColorCode(colorCode); + lc.setP2ScrambleOffset(scrambleOffset); +} + } TEST_CASE("P25 Phase 2 VCH MAC PDU I-OEMI (RS 52,30,23) Test", "[p25][p2_vch_macpdu_ioemi][cap]") {