From dc6d92b70265f07439071079e1daebf10ce8696a Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 14 Jan 2026 15:33:31 -0500 Subject: [PATCH] finally what I was trying to get to -- add testcases for the new P2 MAC PDU logic; correct implementation problems with the P2 MAC PDU handling for OEMI and IEMI (the implementation still requires handling scrambling); --- src/common/p25/P25Defines.h | 19 +- src/common/p25/lc/LC.cpp | 117 ++++++----- src/common/p25/lc/LC.h | 10 +- tests/p25/P2_VCH_MACPDU_Test.cpp | 325 +++++++++++++++++++++++++++++++ 4 files changed, 412 insertions(+), 59 deletions(-) create mode 100644 tests/p25/P2_VCH_MACPDU_Test.cpp diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index a76ae562..9603f63a 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -39,6 +39,7 @@ namespace p25 */ /** @name Frame Lengths and Misc Constants */ + // TIA-102.BAAA-B Section 4 and 5; TIA-102.AABB-B Section 5 const uint32_t P25_HDU_FRAME_LENGTH_BYTES = 99U; const uint32_t P25_HDU_FRAME_LENGTH_BITS = P25_HDU_FRAME_LENGTH_BYTES * 8U; @@ -63,20 +64,28 @@ namespace p25 const uint32_t P25_TDULC_FRAME_LENGTH_BYTES = 54U; const uint32_t P25_TDULC_FRAME_LENGTH_BITS = P25_TDULC_FRAME_LENGTH_BYTES * 8U; + const uint32_t P25_NID_LENGTH_BYTES = 8U; + const uint32_t P25_NID_LENGTH_BITS = P25_NID_LENGTH_BYTES * 8U; + + // 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; const uint32_t P25_P2_IEMI_LENGTH_BITS = 312U; const uint32_t P25_P2_IEMI_LENGTH_BYTES = (P25_P2_IEMI_LENGTH_BITS / 8U) + 1U; - const uint32_t P25_P2_IEMI_MAC_LENGTH_BITS = 148U; + const uint32_t P25_P2_IEMI_WSYNC_LENGTH_BITS = 276U; + const uint32_t P25_P2_IEMI_WSYNC_LENGTH_BYTES = (P25_P2_IEMI_WSYNC_LENGTH_BITS / 8U) + 1U; + + const uint32_t P25_P2_IEMI_MAC_LENGTH_BITS = 156U; const uint32_t P25_P2_IEMI_MAC_LENGTH_BYTES = (P25_P2_IEMI_MAC_LENGTH_BITS / 8U) + 1U; + + const uint32_t P25_P2_SOEMI_MAC_LENGTH_BITS = 156U; + const uint32_t P25_P2_SOEMI_MAC_LENGTH_BYTES = (P25_P2_SOEMI_MAC_LENGTH_BITS / 8U) + 1U; const uint32_t P25_P2_SOEMI_LENGTH_BITS = 270U; const uint32_t P25_P2_SOEMI_LENGTH_BYTES = (P25_P2_SOEMI_LENGTH_BITS / 8U) + 1U; - const uint32_t P25_P2_IOEMI_MAC_LENGTH_BITS = 172U; - const uint32_t P25_P2_IOEMI_MAC_LENGTH_BYTES = (P25_P2_IOEMI_MAC_LENGTH_BITS / 8U) + 1U; - const uint32_t P25_NID_LENGTH_BYTES = 8U; - const uint32_t P25_NID_LENGTH_BITS = P25_NID_LENGTH_BYTES * 8U; + const uint32_t P25_P2_IOEMI_MAC_LENGTH_BITS = 180U; + const uint32_t P25_P2_IOEMI_MAC_LENGTH_BYTES = (P25_P2_IOEMI_MAC_LENGTH_BITS / 8U) + 1U; // TIA-102.BAAA-B Section 7.3 // 5 5 7 5 F 5 F F 7 7 F F diff --git a/src/common/p25/lc/LC.cpp b/src/common/p25/lc/LC.cpp index a01d8698..33159070 100644 --- a/src/common/p25/lc/LC.cpp +++ b/src/common/p25/lc/LC.cpp @@ -486,52 +486,75 @@ void LC::encodeLDU2(uint8_t* data) /* Decode a IEMI VCH MAC PDU. */ -bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data) +bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync) { assert(data != nullptr); + // determine buffer size based on sync flag + uint32_t lengthBits = sync ? P25_P2_IEMI_WSYNC_LENGTH_BITS : P25_P2_IEMI_LENGTH_BITS; + uint32_t lengthBytes = sync ? P25_P2_IEMI_WSYNC_LENGTH_BYTES : P25_P2_IEMI_LENGTH_BYTES; + // decode the Phase 2 DUID - uint8_t duid[1U], raw[P25_P2_IEMI_LENGTH_BYTES]; + uint8_t duid[1U], raw[P25_P2_IEMI_LENGTH_BYTES]; // Use max size for stack allocation ::memset(duid, 0x00U, 1U); - ::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES); + ::memset(raw, 0x00U, lengthBytes); - for (uint8_t i = 0U; i < 8U; i++) { - uint32_t n = i + 72U; // skip field 1 - if (i >= 2U) - n += 72U; // skip field 2 - if (i >= 4U) - n += 96U; // skip field 3 - if (i >= 6U) - n += 72U; // skip field 4 + // DUID bit extraction differs based on sync flag + if (sync) { + // IEMI with sync: 14 LSB sync bits + 36-bit field 1, then DUIDs at different positions + for (uint8_t i = 0U; i < 8U; i++) { + uint32_t n = i + 14U + 36U; // skip 14-bit sync + field 1 (36 bits) + if (i >= 2U) + n += 72U; // skip field 2 + if (i >= 4U) + n += 96U; // skip field 3 + if (i >= 6U) + n += 72U; // skip field 4 - bool b = READ_BIT(data, n); - WRITE_BIT(raw, i, b); + bool b = READ_BIT(data, n); + WRITE_BIT(raw, i, b); + } + } else { + // IEMI without sync: 72-bit field 1, then DUIDs + for (uint8_t i = 0U; i < 8U; i++) { + uint32_t n = i + 72U; // skip field 1 + if (i >= 2U) + n += 72U; // skip field 2 + if (i >= 4U) + n += 96U; // skip field 3 + if (i >= 6U) + n += 72U; // skip field 4 + + bool b = READ_BIT(data, n); + WRITE_BIT(raw, i, b); + } } decodeP2_DUIDHamming(raw, duid); m_p2DUID = duid[0U] >> 4U; - if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V) + if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V || !sync) return true; // don't handle 4V or 2V voice PDUs here -- user code will handle else { - ::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES); - - for (uint32_t i = 0U; i < P25_P2_IEMI_LENGTH_BITS; i++) { - uint32_t n = i; - if (i >= 72U) - n += 2U; // skip DUID 1 - if (i >= 146U) - n += 2U; // skip DUID 2 - if (i >= 242U) - n += 2U; // skip DUID 3 + ::memset(raw, 0x00U, lengthBytes); + + // IEMI with sync: extract data bits (skip 14-bit sync and DUIDs) + for (uint32_t i = 0U; i < lengthBits; i++) { + uint32_t n = i + 14U; // Skip 14-bit sync + if (i >= 36U) + n += 2U; // skip DUID 1 after field 1 (36 bits) + if (i >= 108U) + n += 2U; // skip DUID 2 after field 2 (36+72) + if (i >= 204U) + n += 2U; // skip DUID 3 after field 3 (36+72+96) bool b = READ_BIT(data, n); WRITE_BIT(raw, i, b); } #if DEBUG_P25_MAC_PDU - Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, lengthBytes); #endif // decode RS (46,26,21) FEC @@ -543,12 +566,12 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data) } } catch (...) { - Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), RS excepted with input data", raw, P25_P2_IEMI_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), RS excepted with input data", raw, lengthBytes); return false; } #if DEBUG_P25_MAC_PDU - Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, lengthBytes); #endif // are we decoding a FACCH with scrambling? @@ -561,7 +584,7 @@ bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data) /* TODO: if scrambled handle scrambling */ } - return decodeMACPDU(raw); + return decodeMACPDU(raw, P25_P2_IEMI_MAC_LENGTH_BITS); } return true; @@ -610,8 +633,8 @@ bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync) if (i >= 198U) n += 2U; // skip DUID 3 - bool b = READ_BIT(data, i); - WRITE_BIT(raw, n, b); + bool b = READ_BIT(data, n); + WRITE_BIT(raw, i, b); } #if DEBUG_P25_MAC_PDU @@ -642,8 +665,8 @@ bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync) if (i >= 168U) n += 2U; // skip DUID 3 - bool b = READ_BIT(data, i); - WRITE_BIT(raw, n, b); + bool b = READ_BIT(data, n); + WRITE_BIT(raw, i, b); } #if DEBUG_P25_MAC_PDU @@ -678,7 +701,7 @@ bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync) /* TODO: if scrambled handle scrambling */ } - return decodeMACPDU(raw); + return decodeMACPDU(raw, sync ? P25_P2_SOEMI_MAC_LENGTH_BITS : P25_P2_IOEMI_MAC_LENGTH_BITS); } return true; @@ -694,7 +717,7 @@ void LC::encodeVCH_MACPDU(uint8_t* data, bool sync) ::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES); if (m_p2DUID != P2_DUID::VTCH_4V && m_p2DUID != P2_DUID::VTCH_2V) { - encodeMACPDU(raw, sync); + encodeMACPDU(raw, sync ? P25_P2_SOEMI_MAC_LENGTH_BITS : P25_P2_IOEMI_MAC_LENGTH_BITS); #if DEBUG_P25_MAC_PDU Utils::dump(2U, "P25, LC::encodeVCH_MACPDU(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES); @@ -1057,11 +1080,11 @@ void LC::encodeLC(uint8_t* rs) /* Decode MAC PDU. */ -bool LC::decodeMACPDU(const uint8_t* raw) +bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength) { assert(raw != nullptr); - bool ret = edac::CRC::checkCRC12(raw, P25_P2_IEMI_MAC_LENGTH_BITS); + bool ret = edac::CRC::checkCRC12(raw, macLength - 12U); if (!ret) { if (s_warnCRC) { LogWarning(LOG_P25, "TSBK::decode(), failed CRC CCITT-162 check"); @@ -1173,11 +1196,12 @@ bool LC::decodeMACPDU(const uint8_t* raw) if (p2MCOData != nullptr) delete[] p2MCOData; - p2MCOData = new uint8_t[P25_P2_IOEMI_MAC_LENGTH_BYTES]; - ::memset(p2MCOData, 0x00U, P25_P2_IOEMI_MAC_LENGTH_BYTES); + 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, P25_P2_IEMI_MAC_LENGTH_BYTES - 3U); // excluding MAC PDU header and CRC + ::memcpy(p2MCOData, raw + 1U, macLengthBytes - 3U); // excluding MAC PDU header and CRC } break; @@ -1191,7 +1215,7 @@ bool LC::decodeMACPDU(const uint8_t* raw) /* Encode MAC PDU. */ -void LC::encodeMACPDU(uint8_t* raw, bool sync) +void LC::encodeMACPDU(uint8_t* raw, uint32_t macLength) { assert(raw != nullptr); @@ -1267,11 +1291,8 @@ void LC::encodeMACPDU(uint8_t* raw, bool sync) } else { if (p2MCOData != nullptr) { // this will include the entire MCO (and depending on message length multiple MCOs) - if (sync) { - ::memcpy(raw + 1U, p2MCOData, P25_P2_IEMI_MAC_LENGTH_BYTES - 3U); // excluding MAC PDU header and CRC - } else { - ::memcpy(raw + 1U, p2MCOData, P25_P2_IOEMI_MAC_LENGTH_BYTES - 3U); // excluding MAC PDU header and CRC - } + uint32_t macLengthBytes = (macLength / 8U) + ((macLength % 8U) ? 1U : 0U); + ::memcpy(raw + 1U, p2MCOData, macLengthBytes - 3U); // excluding MAC PDU header and CRC } } break; @@ -1281,11 +1302,7 @@ void LC::encodeMACPDU(uint8_t* raw, bool sync) break; } - if (sync) { - edac::CRC::addCRC12(raw, P25_P2_IEMI_MAC_LENGTH_BITS); - } else { - edac::CRC::addCRC12(raw, P25_P2_IOEMI_MAC_LENGTH_BITS); - } + edac::CRC::addCRC12(raw, macLength - 12U); } /* diff --git a/src/common/p25/lc/LC.h b/src/common/p25/lc/LC.h index feda4d3a..0695a1aa 100644 --- a/src/common/p25/lc/LC.h +++ b/src/common/p25/lc/LC.h @@ -116,9 +116,10 @@ namespace p25 /** * @brief Decode a IEMI VCH MAC PDU. * @param data Buffer containing the MAC PDU to decode. + * @param sync Flag indicating if sync is included (true=276 bits with RS, false=312 bits no RS). * @return bool True, if MAC PDU decoded, otherwise false. */ - bool decodeVCH_MACPDU_IEMI(const uint8_t* data); + bool decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync); /** * @brief Decode a xOEMI VCH MAC PDU. * @param data Buffer containing the MAC PDU to decode. @@ -155,15 +156,16 @@ namespace p25 /** * @brief Decode MAC PDU. * @param[in] raw Buffer containing the decoded Reed-Solomon MAC PDU data. + * @param macLength MAC PDU length in bits (156 for IEMI/S-OEMI, 180 for I-OEMI). * @returns bool True, if MAC PDU is decoded, otherwise false. */ - bool decodeMACPDU(const uint8_t* raw); + bool decodeMACPDU(const uint8_t* raw, uint32_t macLength = defines::P25_P2_IOEMI_MAC_LENGTH_BITS); /** * @brief Encode MAC PDU. * @param[out] raw Buffer to encode MAC PDU data. - * @param sync Flag indicating if sync is to be included. + * @param macLength MAC PDU length in bits (156 for IEMI/S-OEMI, 180 for I-OEMI). */ - void encodeMACPDU(uint8_t* raw, bool sync); + void encodeMACPDU(uint8_t* raw, uint32_t macLength = defines::P25_P2_IOEMI_MAC_LENGTH_BITS); /** @name Encryption data */ /** diff --git a/tests/p25/P2_VCH_MACPDU_Test.cpp b/tests/p25/P2_VCH_MACPDU_Test.cpp new file mode 100644 index 00000000..415e7f4b --- /dev/null +++ b/tests/p25/P2_VCH_MACPDU_Test.cpp @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "host/Defines.h" +#include "common/edac/RS634717.h" +#include "common/p25/P25Defines.h" +#include "common/p25/lc/LC.h" +#include "common/p25/Sync.h" +#include "common/Log.h" +#include "common/Utils.h" + +using namespace edac; +using namespace p25; +using namespace p25::defines; +using namespace p25::lc; + +#include +#include +#include + +TEST_CASE("P25 Phase 2 VCH MAC PDU I-OEMI (RS 52,30,23) Test", "[p25][p2_vch_macpdu_ioemi]") { + bool failed = false; + + INFO("P25 Phase 2 VCH MAC PDU I-OEMI RS (52,30,23) FEC Test"); + + srand((unsigned int)time(NULL)); + + // Create LC instance + LC lc; + + // Set up test MAC PDU data for Phase 2 + lc.setMFId(MFG_STANDARD); + lc.setLCO(P2_MAC_MCO::GROUP); // Phase 2 MAC MCO + lc.setSrcId(1234); + lc.setDstId(9876); + lc.setEmergency(false); + lc.setEncrypted(false); + lc.setPriority(4U); + lc.setGroup(true); + + // Set Phase 2 specific fields + lc.setP2DUID(P2_DUID::FACCH_UNSCRAMBLED); + lc.setMACPDUOpcode(P2_MAC_HEADER_OPCODE::IDLE); // IDLE opcode for MAC PDU + lc.setMACPartition(P2_MAC_MCO_PARTITION::UNIQUE); // UNIQUE partition + + // Encode VCH MAC PDU (I-OEMI, no sync) + uint8_t encodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(encodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + + lc.encodeVCH_MACPDU(encodedData, false); + + Utils::dump(2U, "LC::encodeVCH_MACPDU(), I-OEMI Encoded Data", encodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Inject 2 errors to test error correction (RS can correct up to 11 errors) + uint32_t errorPos1 = 100; + uint32_t errorPos2 = 150; + + uint8_t originalBit1 = READ_BIT(encodedData, errorPos1); + uint8_t originalBit2 = READ_BIT(encodedData, errorPos2); + + WRITE_BIT(encodedData, errorPos1, !originalBit1); + WRITE_BIT(encodedData, errorPos2, !originalBit2); + + Utils::dump(2U, "LC::decodeVCH_MACPDU_OEMI(), I-OEMI Data (errors injected)", encodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Decode VCH MAC PDU (I-OEMI, no sync) + LC decodedLc; + bool ret = decodedLc.decodeVCH_MACPDU_OEMI(encodedData, false); + + if (!ret) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), failed to decode I-OEMI MAC PDU"); + failed = true; + } + + // Verify decoded data matches original + if (decodedLc.getLCO() != lc.getLCO()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), LCO mismatch: expected %02X, got %02X", lc.getLCO(), decodedLc.getLCO()); + failed = true; + } + + if (decodedLc.getSrcId() != lc.getSrcId()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), Source ID mismatch: expected %u, got %u", lc.getSrcId(), decodedLc.getSrcId()); + failed = true; + } + + if (decodedLc.getDstId() != lc.getDstId()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), Dest ID mismatch: expected %u, got %u", lc.getDstId(), decodedLc.getDstId()); + failed = true; + } + + if (decodedLc.getP2DUID() != lc.getP2DUID()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), P2 DUID mismatch: expected %02X, got %02X", lc.getP2DUID(), decodedLc.getP2DUID()); + failed = true; + } + + REQUIRE(failed == false); +} + +TEST_CASE("P25 Phase 2 VCH MAC PDU S-OEMI (RS 45,26,20) Test", "[p25][p2_vch_macpdu_soemi]") { + bool failed = false; + + INFO("P25 Phase 2 VCH MAC PDU S-OEMI RS (45,26,20) FEC Test"); + + srand((unsigned int)time(NULL)); + + // Create LC instance + LC lc; + + // Set up test MAC PDU data for Phase 2 + lc.setMFId(MFG_STANDARD); + lc.setLCO(P2_MAC_MCO::PRIVATE); // Phase 2 MAC MCO + lc.setSrcId(5678); + lc.setDstId(1234); + lc.setEmergency(true); + lc.setEncrypted(true); + lc.setPriority(7U); + lc.setGroup(false); + + // Set Phase 2 specific fields + lc.setP2DUID(P2_DUID::SACCH_UNSCRAMBLED); + lc.setMACPDUOpcode(P2_MAC_HEADER_OPCODE::IDLE); // IDLE opcode for MAC PDU + lc.setMACPartition(P2_MAC_MCO_PARTITION::UNIQUE); // UNIQUE partition + lc.setAlgId(ALGO_UNENCRYPT); // For test, use unencrypted + + // Encode VCH MAC PDU (S-OEMI, with sync) + uint8_t encodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(encodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + + lc.encodeVCH_MACPDU(encodedData, true); + + Utils::dump(2U, "LC::encodeVCH_MACPDU(), S-OEMI Encoded Data", encodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Note: Error injection test is skipped for S-OEMI because hexbit-level errors + // are complex to inject correctly. The I-OEMI test demonstrates RS error correction. + + // Decode VCH MAC PDU (S-OEMI, with sync) without error injection + LC decodedLc; + bool ret = decodedLc.decodeVCH_MACPDU_OEMI(encodedData, true); + + if (!ret) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), failed to decode S-OEMI MAC PDU"); + failed = true; + } + + // Verify decoded data matches original + if (decodedLc.getLCO() != lc.getLCO()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), LCO mismatch: expected %02X, got %02X", lc.getLCO(), decodedLc.getLCO()); + failed = true; + } + + if (decodedLc.getSrcId() != lc.getSrcId()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), Source ID mismatch: expected %u, got %u", lc.getSrcId(), decodedLc.getSrcId()); + failed = true; + } + + if (decodedLc.getDstId() != lc.getDstId()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), Dest ID mismatch: expected %u, got %u", lc.getDstId(), decodedLc.getDstId()); + failed = true; + } + + if (decodedLc.getP2DUID() != lc.getP2DUID()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), P2 DUID mismatch: expected %02X, got %02X", lc.getP2DUID(), decodedLc.getP2DUID()); + failed = true; + } + + if (decodedLc.getEmergency() != lc.getEmergency()) { + ::LogError("T", "LC::decodeVCH_MACPDU_OEMI(), Emergency flag mismatch"); + failed = true; + } + + REQUIRE(failed == false); +} + +TEST_CASE("P25 Phase 2 VCH MAC PDU Round-Trip I-OEMI Test", "[p25][p2_vch_macpdu_roundtrip_ioemi]") { + bool failed = false; + + INFO("P25 Phase 2 VCH MAC PDU I-OEMI Round-Trip Test"); + + // Create LC instance with various configurations + LC lc; + lc.setMFId(MFG_STANDARD); + lc.setLCO(P2_MAC_MCO::GROUP); // Phase 2 MAC MCO + lc.setSrcId(12345); + lc.setDstId(67890); + lc.setEmergency(false); + lc.setEncrypted(false); + lc.setPriority(5U); + lc.setGroup(true); + lc.setP2DUID(P2_DUID::FACCH_UNSCRAMBLED); + lc.setMACPDUOpcode(P2_MAC_HEADER_OPCODE::IDLE); + lc.setMACPartition(P2_MAC_MCO_PARTITION::UNIQUE); + + // Encode without sync (I-OEMI) + uint8_t encodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(encodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + lc.encodeVCH_MACPDU(encodedData, false); + + Utils::dump(2U, "Round-Trip Test: Encoded I-OEMI", encodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Decode + LC decodedLc; + bool ret = decodedLc.decodeVCH_MACPDU_OEMI(encodedData, false); + + if (!ret) { + ::LogError("T", "Round-trip decode failed"); + failed = true; + } + + // Re-encode + uint8_t reencodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(reencodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + decodedLc.encodeVCH_MACPDU(reencodedData, false); + + Utils::dump(2U, "Round-Trip Test: Re-encoded I-OEMI", reencodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Compare original and re-encoded data + for (uint32_t i = 0; i < P25_P2_FRAME_LENGTH_BYTES; i++) { + if (encodedData[i] != reencodedData[i]) { + ::LogError("T", "Round-trip data mismatch at byte %u: expected %02X, got %02X", i, encodedData[i], reencodedData[i]); + failed = true; + break; + } + } + + REQUIRE(failed == false); +} + +TEST_CASE("P25 Phase 2 VCH MAC PDU Round-Trip S-OEMI Test", "[p25][p2_vch_macpdu_roundtrip_soemi]") { + bool failed = false; + + INFO("P25 Phase 2 VCH MAC PDU S-OEMI Round-Trip Test"); + + // Create LC instance with various configurations + LC lc; + lc.setMFId(MFG_STANDARD); + lc.setLCO(P2_MAC_MCO::TEL_INT_VCH_USER); // Phase 2 MAC MCO + lc.setSrcId(11111); + lc.setDstId(22222); + lc.setEmergency(true); + lc.setEncrypted(false); + lc.setPriority(6U); + lc.setGroup(false); + lc.setP2DUID(P2_DUID::SACCH_UNSCRAMBLED); + lc.setMACPDUOpcode(P2_MAC_HEADER_OPCODE::IDLE); + lc.setMACPartition(P2_MAC_MCO_PARTITION::UNIQUE); + + // Encode with sync (S-OEMI) + uint8_t encodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(encodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + lc.encodeVCH_MACPDU(encodedData, true); + + Utils::dump(2U, "Round-Trip Test: Encoded S-OEMI", encodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Decode + LC decodedLc; + bool ret = decodedLc.decodeVCH_MACPDU_OEMI(encodedData, true); + + if (!ret) { + ::LogError("T", "Round-trip decode failed"); + failed = true; + } + + // Re-encode + uint8_t reencodedData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(reencodedData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + decodedLc.encodeVCH_MACPDU(reencodedData, true); + + Utils::dump(2U, "Round-Trip Test: Re-encoded S-OEMI", reencodedData, P25_P2_FRAME_LENGTH_BYTES); + + // Compare original and re-encoded data + for (uint32_t i = 0; i < P25_P2_FRAME_LENGTH_BYTES; i++) { + if (encodedData[i] != reencodedData[i]) { + ::LogError("T", "Round-trip data mismatch at byte %u: expected %02X, got %02X", i, encodedData[i], reencodedData[i]); + failed = true; + break; + } + } + + REQUIRE(failed == false); +} + +TEST_CASE("P25 Phase 2 VCH MAC PDU Voice PDU Bypass Test", "[p25][p2_vch_macpdu_voice_bypass]") { + bool failed = false; + + INFO("P25 Phase 2 VCH MAC PDU Voice PDU Bypass Test"); + + // Test that 4V and 2V voice PDUs are properly bypassed + uint8_t testData[P25_P2_FRAME_LENGTH_BYTES]; + ::memset(testData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + + // Test VTCH_4V bypass + LC lc4v; + lc4v.setP2DUID(P2_DUID::VTCH_4V); + lc4v.encodeVCH_MACPDU(testData, false); + + LC decoded4v; + bool ret4v = decoded4v.decodeVCH_MACPDU_OEMI(testData, false); + + if (!ret4v) { + ::LogError("T", "Failed to handle VTCH_4V bypass"); + failed = true; + } + + // Test VTCH_2V bypass + ::memset(testData, 0x00U, P25_P2_FRAME_LENGTH_BYTES); + LC lc2v; + lc2v.setP2DUID(P2_DUID::VTCH_2V); + lc2v.encodeVCH_MACPDU(testData, false); + + LC decoded2v; + bool ret2v = decoded2v.decodeVCH_MACPDU_OEMI(testData, false); + + if (!ret2v) { + ::LogError("T", "Failed to handle VTCH_2V bypass"); + failed = true; + } + + REQUIRE(failed == false); +}