rebuild r05a04_dev from nasty Git merge bullshit; implement handling of SNDCP on the FNE instead of dvmhost; add quick sanity Catch2 testcases; BUGFIX: NXDN SACCH was incorrectly handling the RAN and structure causing the structure value to become overwritten; correct badly set example IP range in FNE config; add AI generated documentation for the network statck, FNE REST and DVMHost REST; update version number for next dev version;
parent
274a8f23fc
commit
c7b9f80335
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,103 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/edac/BPTC19696.h"
|
||||
|
||||
using namespace edac;
|
||||
|
||||
TEST_CASE("BPTC19696 preserves all-zero payload", "[dmr][bptc19696]") {
|
||||
uint8_t input[12U];
|
||||
::memset(input, 0x00U, sizeof(input));
|
||||
|
||||
uint8_t encoded[196U];
|
||||
BPTC19696 bptc;
|
||||
bptc.encode(input, encoded);
|
||||
|
||||
uint8_t decoded[12U];
|
||||
bptc.decode(encoded, decoded);
|
||||
|
||||
REQUIRE(::memcmp(input, decoded, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BPTC19696 preserves all-ones payload", "[dmr][bptc19696]") {
|
||||
uint8_t input[12U];
|
||||
::memset(input, 0xFFU, sizeof(input));
|
||||
|
||||
uint8_t encoded[196U];
|
||||
BPTC19696 bptc;
|
||||
bptc.encode(input, encoded);
|
||||
|
||||
uint8_t decoded[12U];
|
||||
bptc.decode(encoded, decoded);
|
||||
|
||||
REQUIRE(::memcmp(input, decoded, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BPTC19696 preserves alternating bit pattern", "[dmr][bptc19696]") {
|
||||
uint8_t input[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
input[i] = (i % 2 == 0) ? 0xAAU : 0x55U;
|
||||
}
|
||||
|
||||
uint8_t encoded[196U];
|
||||
BPTC19696 bptc;
|
||||
bptc.encode(input, encoded);
|
||||
|
||||
uint8_t decoded[12U];
|
||||
bptc.decode(encoded, decoded);
|
||||
|
||||
REQUIRE(::memcmp(input, decoded, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BPTC19696 preserves incrementing pattern", "[dmr][bptc19696]") {
|
||||
uint8_t input[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
input[i] = (uint8_t)(i * 17); // Spread values across byte range
|
||||
}
|
||||
|
||||
uint8_t encoded[196U];
|
||||
BPTC19696 bptc;
|
||||
bptc.encode(input, encoded);
|
||||
|
||||
uint8_t decoded[12U];
|
||||
bptc.decode(encoded, decoded);
|
||||
|
||||
REQUIRE(::memcmp(input, decoded, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BPTC19696 corrects single-bit errors", "[dmr][bptc19696]") {
|
||||
uint8_t input[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
input[i] = 0x42U; // Specific pattern
|
||||
}
|
||||
|
||||
uint8_t encoded[196U];
|
||||
BPTC19696 bptc;
|
||||
bptc.encode(input, encoded);
|
||||
|
||||
// Introduce single-bit error in various positions
|
||||
const size_t errorPositions[] = {10, 50, 100, 150, 190};
|
||||
for (auto pos : errorPositions) {
|
||||
uint8_t corrupted[196U];
|
||||
::memcpy(corrupted, encoded, 196U);
|
||||
corrupted[pos] ^= 1U; // Flip one bit
|
||||
|
||||
uint8_t decoded[12U];
|
||||
BPTC19696 bptcDec;
|
||||
bptcDec.decode(corrupted, decoded);
|
||||
|
||||
// Should still match original (or be close - FEC corrects single errors)
|
||||
// Note: This assumes BPTC can correct single-bit errors
|
||||
REQUIRE(::memcmp(input, decoded, 12U) == 0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,384 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "host/Defines.h"
|
||||
#include "common/dmr/DMRDefines.h"
|
||||
#include "common/dmr/lc/csbk/CSBK_RAW.h"
|
||||
#include "common/edac/CRC.h"
|
||||
#include "common/edac/BPTC19696.h"
|
||||
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
using namespace dmr::lc;
|
||||
using namespace dmr::lc::csbk;
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
TEST_CASE("CSBK", "[dmr][csbk]") {
|
||||
SECTION("Constants_Valid") {
|
||||
// Verify CSBK length constants
|
||||
REQUIRE(DMR_CSBK_LENGTH_BYTES == 12);
|
||||
REQUIRE(DMR_FRAME_LENGTH_BYTES == 33);
|
||||
}
|
||||
|
||||
SECTION("Encode_Decode_RoundTrip") {
|
||||
// Test basic encoding/decoding round trip
|
||||
CSBK_RAW csbk1;
|
||||
|
||||
// Create a test CSBK payload (12 bytes)
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
// Set CSBKO (Control Signalling Block Opcode) - byte 0, bits 0-5
|
||||
testCSBK[0] = CSBKO::RAND; // Random Access opcode
|
||||
testCSBK[1] = 0x00; // FID (Feature ID) - standard
|
||||
|
||||
// Set some payload data (bytes 2-9)
|
||||
for (uint32_t i = 2; i < 10; i++) {
|
||||
testCSBK[i] = (uint8_t)(i * 0x11);
|
||||
}
|
||||
|
||||
// Add CRC-CCITT16 (bytes 10-11) with mask
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
// Set the CSBK
|
||||
csbk1.setCSBK(testCSBK);
|
||||
|
||||
// Encode with BPTC (196,96) FEC
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
::memset(encoded, 0x00, DMR_FRAME_LENGTH_BYTES);
|
||||
csbk1.encode(encoded);
|
||||
|
||||
// Decode back
|
||||
CSBK_RAW csbk2;
|
||||
csbk2.setDataType(DataType::CSBK);
|
||||
bool result = csbk2.decode(encoded);
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(csbk2.getCSBKO() == (testCSBK[0] & 0x3F));
|
||||
REQUIRE(csbk2.getFID() == testCSBK[1]);
|
||||
}
|
||||
|
||||
SECTION("LastBlock_Flag") {
|
||||
// Test Last Block Marker flag
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
// Set Last Block flag (bit 7 of byte 0)
|
||||
testCSBK[0] = 0x80 | CSBKO::RAND; // Last Block + CSBKO
|
||||
testCSBK[1] = 0x00;
|
||||
|
||||
// Add CRC
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
REQUIRE(csbk.getLastBlock() == true);
|
||||
REQUIRE(csbk.getCSBKO() == CSBKO::RAND);
|
||||
}
|
||||
|
||||
SECTION("FID_Preservation") {
|
||||
// Test Feature ID preservation
|
||||
uint8_t fids[] = { 0x00, 0x01, 0x10, 0xFF };
|
||||
|
||||
for (auto fid : fids) {
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = CSBKO::RAND;
|
||||
testCSBK[1] = fid;
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
REQUIRE(csbk.getFID() == fid);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("CRC_CCITT16_With_Mask") {
|
||||
// Test CRC-CCITT16 with DMR mask
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = CSBKO::RAND;
|
||||
testCSBK[1] = 0x00;
|
||||
testCSBK[2] = 0xAB;
|
||||
testCSBK[3] = 0xCD;
|
||||
|
||||
// Apply mask before CRC
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
// Add CRC
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
// Verify CRC is valid with mask applied
|
||||
bool crcValid = edac::CRC::checkCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
REQUIRE(crcValid == true);
|
||||
|
||||
// Remove mask
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
// Corrupt the CRC
|
||||
testCSBK[11] ^= 0xFF;
|
||||
|
||||
// Apply mask again
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
// Verify CRC is now invalid
|
||||
crcValid = edac::CRC::checkCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
REQUIRE(crcValid == false);
|
||||
}
|
||||
|
||||
SECTION("Payload_RoundTrip") {
|
||||
// Test payload data round-trip (bytes 2-9, 8 bytes)
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = CSBKO::RAND;
|
||||
testCSBK[1] = 0x00;
|
||||
|
||||
// Payload is bytes 2-9 (8 bytes)
|
||||
uint8_t expectedPayload[8] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF };
|
||||
::memcpy(testCSBK + 2, expectedPayload, 8);
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
// Encode and verify it can be encoded without errors
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
::memset(encoded, 0x00, DMR_FRAME_LENGTH_BYTES);
|
||||
csbk.encode(encoded);
|
||||
|
||||
// Verify BPTC encoding produced non-zero data
|
||||
bool hasData = false;
|
||||
for (uint32_t i = 0; i < DMR_FRAME_LENGTH_BYTES; i++) {
|
||||
if (encoded[i] != 0x00) {
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(hasData == true);
|
||||
}
|
||||
|
||||
SECTION("BPTC_FEC_Encoding") {
|
||||
// Test BPTC (196,96) FEC encoding
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = CSBKO::RAND;
|
||||
testCSBK[1] = 0x00;
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
// Encode with BPTC FEC
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
::memset(encoded, 0x00, DMR_FRAME_LENGTH_BYTES);
|
||||
csbk.encode(encoded);
|
||||
|
||||
// Verify encoding produced data
|
||||
bool hasData = false;
|
||||
for (uint32_t i = 0; i < DMR_FRAME_LENGTH_BYTES; i++) {
|
||||
if (encoded[i] != 0x00) {
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(hasData == true);
|
||||
}
|
||||
|
||||
SECTION("AllZeros_Pattern") {
|
||||
// Test all-zeros pattern
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
csbk.encode(encoded);
|
||||
|
||||
CSBK_RAW csbk2;
|
||||
csbk2.setDataType(DataType::CSBK);
|
||||
bool result = csbk2.decode(encoded);
|
||||
|
||||
REQUIRE(result == true);
|
||||
}
|
||||
|
||||
SECTION("AllOnes_Pattern") {
|
||||
// Test all-ones pattern (with valid structure)
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0xFF, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
// Set CSBKO to DVM_GIT_HASH (0x3F) with Last Block flag
|
||||
testCSBK[0] = 0xBF; // Last Block (0x80) + CSBKO 0x3F = 0xBF
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
csbk.encode(encoded);
|
||||
|
||||
// Verify encoding succeeded
|
||||
bool hasData = false;
|
||||
for (uint32_t i = 0; i < DMR_FRAME_LENGTH_BYTES; i++) {
|
||||
if (encoded[i] != 0x00) {
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(hasData == true);
|
||||
|
||||
// Verify the setCSBK extracted values correctly
|
||||
REQUIRE(csbk.getCSBKO() == 0x3F); // DVM_GIT_HASH
|
||||
REQUIRE(csbk.getLastBlock() == true);
|
||||
}
|
||||
|
||||
SECTION("Alternating_Pattern") {
|
||||
// Test alternating bit pattern
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
for (uint32_t i = 0; i < DMR_CSBK_LENGTH_BYTES; i++) {
|
||||
testCSBK[i] = (i % 2 == 0) ? 0xAA : 0x55;
|
||||
}
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
uint8_t encoded[DMR_FRAME_LENGTH_BYTES];
|
||||
csbk.encode(encoded);
|
||||
|
||||
CSBK_RAW csbk2;
|
||||
csbk2.setDataType(DataType::CSBK);
|
||||
bool result = csbk2.decode(encoded);
|
||||
|
||||
REQUIRE(result == true);
|
||||
}
|
||||
|
||||
SECTION("CSBKO_Values") {
|
||||
// Test various CSBKO values (6 bits)
|
||||
uint8_t csbkoValues[] = {
|
||||
CSBKO::RAND,
|
||||
CSBKO::BSDWNACT,
|
||||
CSBKO::PRECCSBK,
|
||||
0x00, 0x01, 0x0F, 0x20, 0x3F
|
||||
};
|
||||
|
||||
for (uint32_t i = 0; i < sizeof(csbkoValues); i++) {
|
||||
uint8_t csbko = csbkoValues[i];
|
||||
CSBK_RAW csbk;
|
||||
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = csbko & 0x3F; // Mask to 6 bits
|
||||
testCSBK[1] = 0x00;
|
||||
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
testCSBK[10] ^= CSBK_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_CRC_MASK[1];
|
||||
|
||||
csbk.setCSBK(testCSBK);
|
||||
|
||||
REQUIRE(csbk.getCSBKO() == (csbko & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("MBC_CRC_Mask") {
|
||||
// Test MBC (Multi-Block Control) CRC mask variant
|
||||
uint8_t testCSBK[DMR_CSBK_LENGTH_BYTES];
|
||||
::memset(testCSBK, 0x00, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
testCSBK[0] = CSBKO::PRECCSBK; // Preamble CSBK uses MBC header
|
||||
testCSBK[1] = 0x00;
|
||||
|
||||
// Apply MBC mask before CRC
|
||||
testCSBK[10] ^= CSBK_MBC_CRC_MASK[0];
|
||||
testCSBK[11] ^= CSBK_MBC_CRC_MASK[1];
|
||||
|
||||
// Add CRC
|
||||
edac::CRC::addCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
|
||||
// Verify CRC is valid with MBC mask applied
|
||||
bool crcValid = edac::CRC::checkCCITT162(testCSBK, DMR_CSBK_LENGTH_BYTES);
|
||||
REQUIRE(crcValid == true);
|
||||
}
|
||||
|
||||
SECTION("DataType_CSBK") {
|
||||
// Test with DataType::CSBK
|
||||
CSBK_RAW csbk;
|
||||
csbk.setDataType(DataType::CSBK);
|
||||
|
||||
REQUIRE(csbk.getDataType() == DataType::CSBK);
|
||||
}
|
||||
|
||||
SECTION("DataType_MBC_HEADER") {
|
||||
// Test with DataType::MBC_HEADER
|
||||
CSBK_RAW csbk;
|
||||
csbk.setDataType(DataType::MBC_HEADER);
|
||||
|
||||
REQUIRE(csbk.getDataType() == DataType::MBC_HEADER);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/dmr/DMRDefines.h"
|
||||
#include "common/dmr/data/DataHeader.h"
|
||||
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
using namespace dmr::data;
|
||||
|
||||
TEST_CASE("DataHeader encodes and decodes UDT data", "[dmr][dataheader]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
DataHeader hdr;
|
||||
hdr.setDPF(DPF::UDT);
|
||||
hdr.setSAP(0x01U); // UDT SAP is 4 bits (0x0-0xF)
|
||||
hdr.setGI(false);
|
||||
hdr.setSrcId(1001U);
|
||||
hdr.setDstId(2002U);
|
||||
hdr.setBlocksToFollow(3U); // UDT blocks to follow is 2 bits + 1 (1-4 blocks)
|
||||
|
||||
hdr.encode(frame + 2U);
|
||||
|
||||
// Decode and verify
|
||||
DataHeader decoded;
|
||||
REQUIRE(decoded.decode(frame + 2U));
|
||||
REQUIRE(decoded.getDPF() == DPF::UDT);
|
||||
REQUIRE(decoded.getSAP() == 0x01U);
|
||||
REQUIRE(decoded.getGI() == false);
|
||||
REQUIRE(decoded.getSrcId() == 1001U);
|
||||
REQUIRE(decoded.getDstId() == 2002U);
|
||||
REQUIRE(decoded.getBlocksToFollow() == 3U);
|
||||
}
|
||||
|
||||
TEST_CASE("DataHeader encodes and decodes unconfirmed data", "[dmr][dataheader]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
DataHeader hdr;
|
||||
hdr.setDPF(DPF::UNCONFIRMED_DATA);
|
||||
hdr.setSAP(0x00U); // SAP is 4 bits (0x0-0xF)
|
||||
hdr.setGI(true);
|
||||
hdr.setSrcId(5000U);
|
||||
hdr.setDstId(9999U);
|
||||
hdr.setBlocksToFollow(1U);
|
||||
|
||||
hdr.encode(frame + 2U);
|
||||
|
||||
// Decode and verify
|
||||
DataHeader decoded;
|
||||
REQUIRE(decoded.decode(frame + 2U));
|
||||
REQUIRE(decoded.getDPF() == DPF::UNCONFIRMED_DATA);
|
||||
REQUIRE(decoded.getSAP() == 0x00U);
|
||||
REQUIRE(decoded.getGI() == true);
|
||||
REQUIRE(decoded.getSrcId() == 5000U);
|
||||
REQUIRE(decoded.getDstId() == 9999U);
|
||||
REQUIRE(decoded.getBlocksToFollow() == 1U);
|
||||
}
|
||||
|
||||
TEST_CASE("DataHeader handles response headers", "[dmr][dataheader]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
DataHeader hdr;
|
||||
hdr.setDPF(DPF::RESPONSE);
|
||||
hdr.setSAP(0x05U);
|
||||
hdr.setGI(false);
|
||||
hdr.setSrcId(3333U);
|
||||
hdr.setDstId(4444U);
|
||||
hdr.setResponseClass(PDUResponseClass::ACK);
|
||||
hdr.setResponseType(PDUResponseType::ACK);
|
||||
hdr.setResponseStatus(0x00U);
|
||||
hdr.setBlocksToFollow(1U);
|
||||
|
||||
hdr.encode(frame + 2U);
|
||||
|
||||
// Decode and verify
|
||||
DataHeader decoded;
|
||||
REQUIRE(decoded.decode(frame + 2U));
|
||||
REQUIRE(decoded.getDPF() == DPF::RESPONSE);
|
||||
REQUIRE(decoded.getResponseClass() == PDUResponseClass::ACK);
|
||||
REQUIRE(decoded.getResponseType() == PDUResponseType::ACK);
|
||||
}
|
||||
|
||||
TEST_CASE("DataHeader preserves all SAP values", "[dmr][dataheader]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
|
||||
// SAP is 4 bits, valid values are 0x0-0xF
|
||||
const uint8_t sapValues[] = {0x00U, 0x02U, 0x0AU, 0x0DU, 0x0FU};
|
||||
|
||||
for (auto sap : sapValues) {
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
DataHeader hdr;
|
||||
hdr.setDPF(DPF::UDT);
|
||||
hdr.setSAP(sap);
|
||||
hdr.setGI(true);
|
||||
hdr.setSrcId(100U);
|
||||
hdr.setDstId(200U);
|
||||
hdr.setBlocksToFollow(2U);
|
||||
|
||||
hdr.encode(frame + 2U);
|
||||
|
||||
DataHeader decoded;
|
||||
REQUIRE(decoded.decode(frame + 2U));
|
||||
REQUIRE(decoded.getSAP() == sap);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("DataHeader handles maximum blocks to follow", "[dmr][dataheader]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
DataHeader hdr;
|
||||
hdr.setDPF(DPF::UNCONFIRMED_DATA); // Use UNCONFIRMED_DATA which has 7-bit blocks to follow
|
||||
hdr.setSAP(0x00U);
|
||||
hdr.setGI(true);
|
||||
hdr.setSrcId(1U);
|
||||
hdr.setDstId(1U);
|
||||
hdr.setBlocksToFollow(127U); // Max value for 7-bit field
|
||||
|
||||
hdr.encode(frame + 2U);
|
||||
|
||||
DataHeader decoded;
|
||||
REQUIRE(decoded.decode(frame + 2U));
|
||||
REQUIRE(decoded.getBlocksToFollow() == 127U);
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include "common/dmr/lc/FullLC.h"
|
||||
#include "common/dmr/lc/LC.h"
|
||||
#include "common/dmr/DMRDefines.h"
|
||||
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
using namespace dmr::lc;
|
||||
|
||||
TEST_CASE("FullLC encodes and decodes VOICE_LC_HEADER for private call", "[dmr][fulllc]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
uint32_t srcId = 12345U;
|
||||
uint32_t dstId = 54321U;
|
||||
|
||||
LC lc(FLCO::PRIVATE, srcId, dstId);
|
||||
lc.setFID(0x10U);
|
||||
|
||||
FullLC fullLC;
|
||||
fullLC.encode(lc, frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
|
||||
// Decode and verify
|
||||
std::unique_ptr<LC> decoded = fullLC.decode(frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
REQUIRE(decoded != nullptr);
|
||||
REQUIRE(decoded->getFLCO() == FLCO::PRIVATE);
|
||||
REQUIRE(decoded->getSrcId() == srcId);
|
||||
REQUIRE(decoded->getDstId() == dstId);
|
||||
REQUIRE(decoded->getFID() == 0x10U);
|
||||
}
|
||||
|
||||
TEST_CASE("FullLC encodes and decodes VOICE_LC_HEADER for group call", "[dmr][fulllc]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
uint32_t srcId = 1001U;
|
||||
uint32_t dstId = 9999U;
|
||||
|
||||
LC lc(FLCO::GROUP, srcId, dstId);
|
||||
lc.setFID(0x00U);
|
||||
|
||||
FullLC fullLC;
|
||||
fullLC.encode(lc, frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
|
||||
// Decode and verify
|
||||
std::unique_ptr<LC> decoded = fullLC.decode(frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
REQUIRE(decoded != nullptr);
|
||||
REQUIRE(decoded->getFLCO() == FLCO::GROUP);
|
||||
REQUIRE(decoded->getSrcId() == srcId);
|
||||
REQUIRE(decoded->getDstId() == dstId);
|
||||
}
|
||||
|
||||
TEST_CASE("FullLC encodes and decodes TERMINATOR_WITH_LC", "[dmr][fulllc]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
uint32_t srcId = 7777U;
|
||||
uint32_t dstId = 8888U;
|
||||
|
||||
LC lc(FLCO::GROUP, srcId, dstId);
|
||||
lc.setFID(0x02U);
|
||||
|
||||
FullLC fullLC;
|
||||
fullLC.encode(lc, frame + 2U, DataType::TERMINATOR_WITH_LC);
|
||||
|
||||
// Decode and verify
|
||||
std::unique_ptr<LC> decoded = fullLC.decode(frame + 2U, DataType::TERMINATOR_WITH_LC);
|
||||
REQUIRE(decoded != nullptr);
|
||||
REQUIRE(decoded->getFLCO() == FLCO::GROUP);
|
||||
REQUIRE(decoded->getSrcId() == srcId);
|
||||
REQUIRE(decoded->getDstId() == dstId);
|
||||
REQUIRE(decoded->getFID() == 0x02U);
|
||||
}
|
||||
|
||||
TEST_CASE("FullLC preserves service options", "[dmr][fulllc]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
uint32_t srcId = 100U;
|
||||
uint32_t dstId = 200U;
|
||||
|
||||
LC lc(FLCO::PRIVATE, srcId, dstId);
|
||||
lc.setFID(0x01U);
|
||||
lc.setEmergency(true);
|
||||
lc.setEncrypted(true);
|
||||
lc.setPriority(3U);
|
||||
|
||||
FullLC fullLC;
|
||||
fullLC.encode(lc, frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
|
||||
// Decode and verify options
|
||||
std::unique_ptr<LC> decoded = fullLC.decode(frame + 2U, DataType::VOICE_LC_HEADER);
|
||||
REQUIRE(decoded != nullptr);
|
||||
REQUIRE(decoded->getEmergency() == true);
|
||||
REQUIRE(decoded->getEncrypted() == true);
|
||||
REQUIRE(decoded->getPriority() == 3U);
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/dmr/SlotType.h"
|
||||
#include "common/dmr/DMRDefines.h"
|
||||
|
||||
using namespace dmr;
|
||||
using namespace dmr::defines;
|
||||
|
||||
TEST_CASE("SlotType encodes and decodes DataType correctly", "[dmr][slottype]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
SlotType slotType;
|
||||
slotType.setColorCode(5U);
|
||||
slotType.setDataType(DataType::VOICE_LC_HEADER);
|
||||
slotType.encode(frame + 2U);
|
||||
|
||||
// Decode and verify
|
||||
SlotType decoded;
|
||||
decoded.decode(frame + 2U);
|
||||
REQUIRE(decoded.getColorCode() == 5U);
|
||||
REQUIRE(decoded.getDataType() == DataType::VOICE_LC_HEADER);
|
||||
}
|
||||
|
||||
TEST_CASE("SlotType handles all DataType values", "[dmr][slottype]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
|
||||
const DataType::E types[] = {
|
||||
DataType::VOICE_PI_HEADER,
|
||||
DataType::VOICE_LC_HEADER,
|
||||
DataType::TERMINATOR_WITH_LC,
|
||||
DataType::CSBK,
|
||||
DataType::DATA_HEADER,
|
||||
DataType::RATE_12_DATA,
|
||||
DataType::RATE_34_DATA,
|
||||
DataType::IDLE,
|
||||
DataType::RATE_1_DATA
|
||||
};
|
||||
|
||||
for (auto type : types) {
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
SlotType slotType;
|
||||
slotType.setColorCode(3U);
|
||||
slotType.setDataType(type);
|
||||
slotType.encode(frame + 2U);
|
||||
|
||||
SlotType decoded;
|
||||
decoded.decode(frame + 2U);
|
||||
REQUIRE(decoded.getColorCode() == 3U);
|
||||
REQUIRE(decoded.getDataType() == type);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("SlotType handles all valid ColorCode values", "[dmr][slottype]") {
|
||||
uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
|
||||
for (uint32_t cc = 0U; cc <= 15U; cc++) {
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
SlotType slotType;
|
||||
slotType.setColorCode(cc);
|
||||
slotType.setDataType(DataType::CSBK);
|
||||
slotType.encode(frame + 2U);
|
||||
|
||||
SlotType decoded;
|
||||
decoded.decode(frame + 2U);
|
||||
REQUIRE(decoded.getColorCode() == cc);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/edac/RS634717.h"
|
||||
|
||||
using namespace edac;
|
||||
|
||||
TEST_CASE("RS241213 preserves all-zero payload", "[edac][rs241213]") {
|
||||
uint8_t data[24U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
|
||||
REQUIRE(rs.decode241213(data));
|
||||
|
||||
for (size_t i = 0; i < 9U; i++) {
|
||||
REQUIRE(data[i] == 0x00U);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS241213 preserves all-ones payload", "[edac][rs241213]") {
|
||||
uint8_t data[24U];
|
||||
::memset(data, 0xFFU, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
Utils::dump(2U, "encode241213()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode241213(data));
|
||||
|
||||
for (size_t i = 0; i < 9U; i++) {
|
||||
REQUIRE(data[i] == 0xFFU);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS241213 preserves alternating pattern", "[edac][rs241213]") {
|
||||
uint8_t original[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
original[i] = (i % 2 == 0) ? 0xAAU : 0x55U;
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 12U);
|
||||
::memset(data + 12U, 0x00U, 12U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
Utils::dump(2U, "encode241213()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode241213(data));
|
||||
|
||||
// Verify data portion matches original
|
||||
REQUIRE(::memcmp(data, original, 9U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS241213 preserves incrementing pattern", "[edac][rs241213]") {
|
||||
uint8_t original[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
original[i] = (uint8_t)(i * 21); // Spread across byte range
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 12U);
|
||||
::memset(data + 12U, 0x00U, 12U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
Utils::dump(2U, "encode241213()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode241213(data));
|
||||
|
||||
REQUIRE(::memcmp(data, original, 9U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS241213 corrects single-byte errors", "[edac][rs241213]") {
|
||||
uint8_t original[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
original[i] = (uint8_t)(i + 100);
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 12U);
|
||||
::memset(data + 12U, 0x00U, 12U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
Utils::dump(2U, "encode241213()", data, 24U);
|
||||
|
||||
// Introduce single-byte errors at various positions
|
||||
const size_t errorPositions[] = {0, 5, 11, 15, 20};
|
||||
for (auto pos : errorPositions) {
|
||||
uint8_t corrupted[24U];
|
||||
::memcpy(corrupted, data, 24U);
|
||||
corrupted[pos] ^= 0xFFU; // Flip all bits in one byte
|
||||
|
||||
RS634717 rsDec;
|
||||
bool decoded = rsDec.decode241213(corrupted);
|
||||
|
||||
// RS(24,12,13) can correct up to 6 symbol errors
|
||||
if (decoded) {
|
||||
REQUIRE(::memcmp(corrupted, original, 9U) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS241213 detects uncorrectable errors", "[edac][rs241213]") {
|
||||
uint8_t original[12U];
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
original[i] = (uint8_t)(i * 17);
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 12U);
|
||||
::memset(data + 12U, 0x00U, 12U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode241213(data);
|
||||
Utils::dump(2U, "encode241213()", data, 9U);
|
||||
|
||||
// Introduce too many errors (beyond correction capability)
|
||||
for (size_t i = 0; i < 10U; i++) {
|
||||
data[i] ^= 0xFFU;
|
||||
}
|
||||
|
||||
// Should fail to decode
|
||||
bool result = rs.decode241213(data);
|
||||
REQUIRE(!result);
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/edac/RS634717.h"
|
||||
|
||||
using namespace edac;
|
||||
|
||||
TEST_CASE("RS24169 preserves all-zero payload", "[edac][rs24169]") {
|
||||
uint8_t data[24U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode24169(data));
|
||||
|
||||
// First 16 bytes should be zero (data portion)
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
REQUIRE(data[i] == 0x00U);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS24169 preserves all-ones payload", "[edac][rs24169]") {
|
||||
uint8_t data[24U];
|
||||
::memset(data, 0xFFU, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode24169(data));
|
||||
|
||||
// First 16 bytes should be 0xFF
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
REQUIRE(data[i] == 0xFFU);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS24169 preserves alternating pattern", "[edac][rs24169]") {
|
||||
uint8_t original[16U];
|
||||
for (size_t i = 0; i < 16U; i++) {
|
||||
original[i] = (i % 2 == 0) ? 0xAAU : 0x55U;
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 16U);
|
||||
::memset(data + 16U, 0x00U, 8U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode24169(data));
|
||||
|
||||
REQUIRE(::memcmp(data, original, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS24169 preserves incrementing pattern", "[edac][rs24169]") {
|
||||
uint8_t original[16U];
|
||||
for (size_t i = 0; i < 16U; i++) {
|
||||
original[i] = (uint8_t)(i * 16);
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 16U);
|
||||
::memset(data + 16U, 0x00U, 8U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
REQUIRE(rs.decode24169(data));
|
||||
|
||||
REQUIRE(::memcmp(data, original, 12U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS24169 corrects single-byte errors", "[edac][rs24169]") {
|
||||
uint8_t original[16U];
|
||||
for (size_t i = 0; i < 16U; i++) {
|
||||
original[i] = (uint8_t)(i + 50);
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 16U);
|
||||
::memset(data + 16U, 0x00U, 8U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
// Introduce single-byte errors
|
||||
const size_t errorPositions[] = {0, 8, 15, 18, 22};
|
||||
for (auto pos : errorPositions) {
|
||||
uint8_t corrupted[24U];
|
||||
::memcpy(corrupted, data, 24U);
|
||||
corrupted[pos] ^= 0xFFU;
|
||||
|
||||
RS634717 rsDec;
|
||||
bool decoded = rsDec.decode24169(corrupted);
|
||||
|
||||
// RS(24,16,9) can correct up to 4 symbol errors
|
||||
if (decoded) {
|
||||
REQUIRE(::memcmp(corrupted, original, 12U) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS24169 detects uncorrectable errors", "[edac][rs24169]") {
|
||||
uint8_t original[16U];
|
||||
for (size_t i = 0; i < 16U; i++) {
|
||||
original[i] = (uint8_t)(i * 13);
|
||||
}
|
||||
|
||||
uint8_t data[24U];
|
||||
::memcpy(data, original, 16U);
|
||||
::memset(data + 16U, 0x00U, 8U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode24169(data);
|
||||
Utils::dump(2U, "encode24169()", data, 24U);
|
||||
|
||||
// Introduce too many errors
|
||||
for (size_t i = 0; i < 8U; i++) {
|
||||
data[i] ^= 0xFFU;
|
||||
}
|
||||
|
||||
bool result = rs.decode24169(data);
|
||||
REQUIRE(!result);
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/edac/RS634717.h"
|
||||
|
||||
using namespace edac;
|
||||
|
||||
TEST_CASE("RS362017 preserves all-zero payload", "[edac][rs362017]") {
|
||||
uint8_t data[27U]; // 36 symbols * 6 bits = 216 bits = 27 bytes
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
REQUIRE(rs.decode362017(data));
|
||||
|
||||
// First 15 bytes (20 symbols * 6 bits = 120 bits) should be zero
|
||||
for (size_t i = 0; i < 15U; i++) {
|
||||
REQUIRE(data[i] == 0x00U);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS362017 preserves all-ones payload", "[edac][rs362017]") {
|
||||
uint8_t data[27U];
|
||||
::memset(data, 0xFFU, sizeof(data));
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
REQUIRE(rs.decode362017(data));
|
||||
|
||||
// First 15 bytes should be 0xFF
|
||||
for (size_t i = 0; i < 15U; i++) {
|
||||
REQUIRE(data[i] == 0xFFU);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS362017 preserves alternating pattern", "[edac][rs362017]") {
|
||||
uint8_t original[27U];
|
||||
for (size_t i = 0; i < 27U; i++) {
|
||||
original[i] = (i % 2 == 0) ? 0xAAU : 0x55U;
|
||||
}
|
||||
|
||||
uint8_t data[27U];
|
||||
::memcpy(data, original, 27U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
REQUIRE(rs.decode362017(data));
|
||||
|
||||
// Verify first 15 bytes (data portion) match
|
||||
REQUIRE(::memcmp(data, original, 15U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS362017 preserves incrementing pattern", "[edac][rs362017]") {
|
||||
uint8_t original[27U];
|
||||
for (size_t i = 0; i < 27U; i++) {
|
||||
original[i] = (uint8_t)(i * 9);
|
||||
}
|
||||
|
||||
uint8_t data[27U];
|
||||
::memcpy(data, original, 27U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
REQUIRE(rs.decode362017(data));
|
||||
|
||||
REQUIRE(::memcmp(data, original, 15U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RS362017 corrects symbol errors", "[edac][rs362017]") {
|
||||
uint8_t original[27U];
|
||||
for (size_t i = 0; i < 27U; i++) {
|
||||
original[i] = (uint8_t)(i + 30);
|
||||
}
|
||||
|
||||
uint8_t data[27U];
|
||||
::memcpy(data, original, 27U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
// Save encoded data
|
||||
uint8_t encoded[27U];
|
||||
::memcpy(encoded, data, 27U);
|
||||
|
||||
// Introduce errors at various positions
|
||||
const size_t errorPositions[] = {0, 5, 10, 15, 20};
|
||||
for (auto pos : errorPositions) {
|
||||
uint8_t corrupted[27U];
|
||||
::memcpy(corrupted, encoded, 27U);
|
||||
corrupted[pos] ^= 0x3FU; // Flip 6 bits (1 symbol)
|
||||
|
||||
RS634717 rsDec;
|
||||
bool decoded = rsDec.decode362017(corrupted);
|
||||
|
||||
// RS(36,20,17) can correct up to 8 symbol errors
|
||||
if (decoded) {
|
||||
REQUIRE(::memcmp(corrupted, original, 15U) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RS362017 detects uncorrectable errors", "[edac][rs362017]") {
|
||||
uint8_t original[27U];
|
||||
for (size_t i = 0; i < 27U; i++) {
|
||||
original[i] = (uint8_t)(i * 11);
|
||||
}
|
||||
|
||||
uint8_t data[27U];
|
||||
::memcpy(data, original, 27U);
|
||||
|
||||
RS634717 rs;
|
||||
rs.encode362017(data);
|
||||
Utils::dump(2U, "encode362017()", data, 27U);
|
||||
|
||||
// Introduce too many errors (beyond 8 symbol correction)
|
||||
for (size_t i = 0; i < 12U; i++) {
|
||||
data[i] ^= 0xFFU;
|
||||
}
|
||||
|
||||
bool result = rs.decode362017(data);
|
||||
REQUIRE(!result);
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/nxdn/channel/FACCH1.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
|
||||
using namespace nxdn;
|
||||
using namespace nxdn::defines;
|
||||
using namespace nxdn::channel;
|
||||
|
||||
TEST_CASE("FACCH1 encodes and decodes zeros", "[nxdn][facch1]") {
|
||||
uint8_t dataIn[10U];
|
||||
::memset(dataIn, 0x00U, sizeof(dataIn));
|
||||
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
FACCH1 facch;
|
||||
facch.setData(dataIn);
|
||||
facch.encode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
|
||||
// Decode and verify
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(dataIn, dataOut, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 encodes and decodes ones", "[nxdn][facch1]") {
|
||||
uint8_t dataIn[10U];
|
||||
::memset(dataIn, 0xFFU, sizeof(dataIn));
|
||||
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
FACCH1 facch;
|
||||
facch.setData(dataIn);
|
||||
facch.encode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(dataIn, dataOut, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 encodes and decodes alternating pattern", "[nxdn][facch1]") {
|
||||
uint8_t dataIn[10U] = {0xAAU, 0x55U, 0xAAU, 0x55U, 0xAAU, 0x55U, 0xAAU, 0x55U, 0xAAU, 0x55U};
|
||||
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
FACCH1 facch;
|
||||
facch.setData(dataIn);
|
||||
facch.encode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(dataIn, dataOut, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 handles sequential data patterns", "[nxdn][facch1]") {
|
||||
const uint8_t patterns[][10] = {
|
||||
{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99},
|
||||
{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22},
|
||||
{0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66}
|
||||
};
|
||||
|
||||
for (const auto& pattern : patterns) {
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
FACCH1 facch;
|
||||
facch.setData(pattern);
|
||||
facch.encode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(pattern, dataOut, 10U) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 decodes at alternate bit offset", "[nxdn][facch1]") {
|
||||
uint8_t dataIn[10U] = {0xA5, 0x5A, 0xF0, 0x0F, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};
|
||||
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
// Encode at second FACCH1 position
|
||||
FACCH1 facch;
|
||||
facch.setData(dataIn);
|
||||
const uint32_t secondOffset = NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS;
|
||||
facch.encode(frameData, secondOffset);
|
||||
|
||||
// Decode from second position
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, secondOffset));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(dataIn, dataOut, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 copy constructor preserves data", "[nxdn][facch1]") {
|
||||
uint8_t testData[10U] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA};
|
||||
|
||||
FACCH1 original;
|
||||
original.setData(testData);
|
||||
|
||||
FACCH1 copy(original);
|
||||
|
||||
uint8_t originalData[10U], copyData[10U];
|
||||
original.getData(originalData);
|
||||
copy.getData(copyData);
|
||||
REQUIRE(::memcmp(originalData, copyData, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 assignment operator preserves data", "[nxdn][facch1]") {
|
||||
uint8_t testData[10U] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33};
|
||||
|
||||
FACCH1 original;
|
||||
original.setData(testData);
|
||||
|
||||
FACCH1 assigned;
|
||||
assigned = original;
|
||||
|
||||
uint8_t originalData[10U], assignedData[10U];
|
||||
original.getData(originalData);
|
||||
assigned.getData(assignedData);
|
||||
REQUIRE(::memcmp(originalData, assignedData, 10U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 rejects invalid CRC", "[nxdn][facch1]") {
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0xFFU, sizeof(frameData));
|
||||
|
||||
// Create random corrupted data that should fail CRC
|
||||
for (uint32_t i = 0; i < NXDN_FACCH1_FEC_LENGTH_BYTES; i++) {
|
||||
frameData[i] = static_cast<uint8_t>(i * 17 + 23);
|
||||
}
|
||||
|
||||
FACCH1 decoded;
|
||||
// Decode may succeed or fail depending on corruption, but this tests the CRC validation path
|
||||
decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
}
|
||||
|
||||
TEST_CASE("FACCH1 golden test for voice call header", "[nxdn][facch1][golden]") {
|
||||
uint8_t dataIn[10U];
|
||||
::memset(dataIn, 0x00U, sizeof(dataIn));
|
||||
// Simulate RTCH header structure
|
||||
dataIn[0] = MessageType::RTCH_VCALL; // Message Type
|
||||
dataIn[1] = 0x00; // Options
|
||||
dataIn[2] = 0x12; // Source ID (high)
|
||||
dataIn[3] = 0x34; // Source ID (low)
|
||||
dataIn[4] = 0x56; // Dest ID (high)
|
||||
dataIn[5] = 0x78; // Dest ID (low)
|
||||
|
||||
uint8_t frameData[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frameData, 0x00U, sizeof(frameData));
|
||||
|
||||
FACCH1 facch;
|
||||
facch.setData(dataIn);
|
||||
facch.encode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
|
||||
// Decode and verify round-trip
|
||||
FACCH1 decoded;
|
||||
REQUIRE(decoded.decode(frameData, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS));
|
||||
|
||||
uint8_t dataOut[10U];
|
||||
decoded.getData(dataOut);
|
||||
REQUIRE(::memcmp(dataIn, dataOut, 10U) == 0);
|
||||
}
|
||||
@ -0,0 +1,210 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/nxdn/channel/LICH.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
|
||||
using namespace nxdn;
|
||||
using namespace nxdn::defines;
|
||||
using namespace nxdn::channel;
|
||||
|
||||
TEST_CASE("LICH encodes and decodes RCCH channel", "[nxdn][lich]") {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RCCH);
|
||||
lich.setFCT(FuncChannelType::CAC_OUTBOUND);
|
||||
lich.setOption(ChOption::DATA_COMMON);
|
||||
lich.setOutbound(true);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
// Decode and verify
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRFCT() == RFChannelType::RCCH);
|
||||
REQUIRE(decoded.getFCT() == FuncChannelType::CAC_OUTBOUND);
|
||||
REQUIRE(decoded.getOption() == ChOption::DATA_COMMON);
|
||||
REQUIRE(decoded.getOutbound() == true);
|
||||
}
|
||||
|
||||
TEST_CASE("LICH encodes and decodes RDCH voice channel", "[nxdn][lich]") {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RDCH);
|
||||
lich.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(ChOption::STEAL_FACCH);
|
||||
lich.setOutbound(false);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRFCT() == RFChannelType::RDCH);
|
||||
REQUIRE(decoded.getFCT() == FuncChannelType::USC_SACCH_NS);
|
||||
REQUIRE(decoded.getOption() == ChOption::STEAL_FACCH);
|
||||
REQUIRE(decoded.getOutbound() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("LICH preserves all RFChannelType values", "[nxdn][lich]") {
|
||||
const RFChannelType::E rfctValues[] = {
|
||||
RFChannelType::RCCH,
|
||||
RFChannelType::RTCH,
|
||||
RFChannelType::RDCH
|
||||
};
|
||||
|
||||
for (auto rfct : rfctValues) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(rfct);
|
||||
lich.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(ChOption::DATA_NORMAL);
|
||||
lich.setOutbound(true);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRFCT() == rfct);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("LICH preserves all FuncChannelType values", "[nxdn][lich]") {
|
||||
const FuncChannelType::E fctValues[] = {
|
||||
FuncChannelType::CAC_OUTBOUND,
|
||||
FuncChannelType::CAC_INBOUND_LONG,
|
||||
FuncChannelType::CAC_INBOUND_SHORT,
|
||||
FuncChannelType::USC_SACCH_NS,
|
||||
FuncChannelType::USC_UDCH,
|
||||
FuncChannelType::USC_SACCH_SS,
|
||||
FuncChannelType::USC_SACCH_SS_IDLE
|
||||
};
|
||||
|
||||
for (auto fct : fctValues) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RDCH);
|
||||
lich.setFCT(fct);
|
||||
lich.setOption(ChOption::DATA_NORMAL);
|
||||
lich.setOutbound(true);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getFCT() == fct);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("LICH preserves all ChOption values", "[nxdn][lich]") {
|
||||
const ChOption::E optionValues[] = {
|
||||
ChOption::DATA_NORMAL,
|
||||
ChOption::DATA_COMMON,
|
||||
ChOption::STEAL_FACCH,
|
||||
ChOption::STEAL_FACCH1_1,
|
||||
ChOption::STEAL_FACCH1_2
|
||||
};
|
||||
|
||||
for (auto option : optionValues) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RDCH);
|
||||
lich.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(option);
|
||||
lich.setOutbound(true);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getOption() == option);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("LICH preserves outbound flag", "[nxdn][lich]") {
|
||||
for (bool outbound : {true, false}) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RDCH);
|
||||
lich.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(ChOption::DATA_NORMAL);
|
||||
lich.setOutbound(outbound);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getOutbound() == outbound);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("LICH copy constructor preserves all fields", "[nxdn][lich]") {
|
||||
LICH original;
|
||||
original.setRFCT(RFChannelType::RDCH);
|
||||
original.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
original.setOption(ChOption::STEAL_FACCH);
|
||||
original.setOutbound(false);
|
||||
|
||||
LICH copy(original);
|
||||
REQUIRE(copy.getRFCT() == original.getRFCT());
|
||||
REQUIRE(copy.getFCT() == original.getFCT());
|
||||
REQUIRE(copy.getOption() == original.getOption());
|
||||
REQUIRE(copy.getOutbound() == original.getOutbound());
|
||||
}
|
||||
|
||||
TEST_CASE("LICH assignment operator preserves all fields", "[nxdn][lich]") {
|
||||
LICH original;
|
||||
original.setRFCT(RFChannelType::RCCH);
|
||||
original.setFCT(FuncChannelType::CAC_OUTBOUND);
|
||||
original.setOption(ChOption::DATA_COMMON);
|
||||
original.setOutbound(true);
|
||||
|
||||
LICH assigned;
|
||||
assigned = original;
|
||||
REQUIRE(assigned.getRFCT() == original.getRFCT());
|
||||
REQUIRE(assigned.getFCT() == original.getFCT());
|
||||
REQUIRE(assigned.getOption() == original.getOption());
|
||||
REQUIRE(assigned.getOutbound() == original.getOutbound());
|
||||
}
|
||||
|
||||
TEST_CASE("LICH golden test for voice call", "[nxdn][lich][golden]") {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
LICH lich;
|
||||
lich.setRFCT(RFChannelType::RDCH);
|
||||
lich.setFCT(FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(ChOption::STEAL_FACCH);
|
||||
lich.setOutbound(false);
|
||||
|
||||
lich.encode(data);
|
||||
|
||||
// Decode and verify round-trip
|
||||
LICH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRFCT() == RFChannelType::RDCH);
|
||||
REQUIRE(decoded.getFCT() == FuncChannelType::USC_SACCH_NS);
|
||||
REQUIRE(decoded.getOption() == ChOption::STEAL_FACCH);
|
||||
REQUIRE(decoded.getOutbound() == false);
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/nxdn/lc/RTCH.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
|
||||
using namespace nxdn;
|
||||
using namespace nxdn::defines;
|
||||
using namespace nxdn::lc;
|
||||
|
||||
TEST_CASE("RTCH encodes and decodes voice call", "[nxdn][rtch]") {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(MessageType::RTCH_VCALL);
|
||||
rtch.setSrcId(12345U);
|
||||
rtch.setDstId(54321U);
|
||||
rtch.setEmergency(false);
|
||||
rtch.setPriority(false);
|
||||
rtch.setDuplex(true);
|
||||
rtch.setTransmissionMode(TransmissionMode::MODE_4800);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
// Decode and verify
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getMessageType() == MessageType::RTCH_VCALL);
|
||||
REQUIRE(decoded.getSrcId() == 12345U);
|
||||
REQUIRE(decoded.getDstId() == 54321U);
|
||||
REQUIRE(decoded.getEmergency() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH preserves all MessageType values", "[nxdn][rtch]") {
|
||||
const uint8_t messageTypes[] = {
|
||||
MessageType::RTCH_VCALL,
|
||||
MessageType::RTCH_VCALL_IV,
|
||||
MessageType::RTCH_TX_REL,
|
||||
MessageType::RTCH_TX_REL_EX,
|
||||
MessageType::RTCH_DCALL_HDR,
|
||||
MessageType::RTCH_DCALL_DATA
|
||||
};
|
||||
|
||||
for (auto messageType : messageTypes) {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(messageType);
|
||||
rtch.setSrcId(1234U);
|
||||
rtch.setDstId(5678U);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getMessageType() == messageType);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH preserves source and destination IDs", "[nxdn][rtch]") {
|
||||
const uint32_t testIds[] = {0U, 1U, 255U, 1000U, 32767U, 65535U};
|
||||
|
||||
for (auto srcId : testIds) {
|
||||
for (auto dstId : testIds) {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(MessageType::RTCH_VCALL);
|
||||
rtch.setSrcId(srcId);
|
||||
rtch.setDstId(dstId);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getSrcId() == srcId);
|
||||
REQUIRE(decoded.getDstId() == dstId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH preserves emergency flag", "[nxdn][rtch]") {
|
||||
for (bool isEmergency : {true, false}) {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(MessageType::RTCH_VCALL);
|
||||
rtch.setSrcId(100U);
|
||||
rtch.setDstId(200U);
|
||||
rtch.setEmergency(isEmergency);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getEmergency() == isEmergency);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH preserves duplex flag", "[nxdn][rtch]") {
|
||||
for (bool isDuplex : {true, false}) {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(MessageType::RTCH_VCALL);
|
||||
rtch.setSrcId(100U);
|
||||
rtch.setDstId(200U);
|
||||
rtch.setDuplex(isDuplex);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getDuplex() == isDuplex);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH preserves transmission mode", "[nxdn][rtch]") {
|
||||
const uint8_t transmissionModes[] = {
|
||||
TransmissionMode::MODE_4800,
|
||||
TransmissionMode::MODE_9600
|
||||
};
|
||||
|
||||
for (auto mode : transmissionModes) {
|
||||
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
RTCH rtch;
|
||||
rtch.setMessageType(MessageType::RTCH_VCALL);
|
||||
rtch.setSrcId(100U);
|
||||
rtch.setDstId(200U);
|
||||
rtch.setTransmissionMode(mode);
|
||||
|
||||
rtch.encode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
RTCH decoded;
|
||||
decoded.decode(data, NXDN_RTCH_LC_LENGTH_BITS);
|
||||
REQUIRE(decoded.getTransmissionMode() == mode);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH copy constructor preserves all fields", "[nxdn][rtch]") {
|
||||
RTCH original;
|
||||
original.setMessageType(MessageType::RTCH_VCALL);
|
||||
original.setSrcId(11111U);
|
||||
original.setDstId(22222U);
|
||||
original.setGroup(true);
|
||||
original.setEmergency(true);
|
||||
original.setEncrypted(false);
|
||||
original.setPriority(true);
|
||||
|
||||
RTCH copy(original);
|
||||
REQUIRE(copy.getMessageType() == original.getMessageType());
|
||||
REQUIRE(copy.getSrcId() == original.getSrcId());
|
||||
REQUIRE(copy.getDstId() == original.getDstId());
|
||||
REQUIRE(copy.getGroup() == original.getGroup());
|
||||
REQUIRE(copy.getEmergency() == original.getEmergency());
|
||||
REQUIRE(copy.getEncrypted() == original.getEncrypted());
|
||||
}
|
||||
|
||||
TEST_CASE("RTCH assignment operator preserves all fields", "[nxdn][rtch]") {
|
||||
RTCH original;
|
||||
original.setMessageType(MessageType::RTCH_TX_REL);
|
||||
original.setSrcId(9999U);
|
||||
original.setDstId(8888U);
|
||||
original.setGroup(false);
|
||||
original.setEmergency(false);
|
||||
original.setEncrypted(true);
|
||||
|
||||
RTCH assigned;
|
||||
assigned = original;
|
||||
REQUIRE(assigned.getMessageType() == original.getMessageType());
|
||||
REQUIRE(assigned.getSrcId() == original.getSrcId());
|
||||
REQUIRE(assigned.getDstId() == original.getDstId());
|
||||
REQUIRE(assigned.getGroup() == original.getGroup());
|
||||
REQUIRE(assigned.getEmergency() == original.getEmergency());
|
||||
REQUIRE(assigned.getEncrypted() == original.getEncrypted());
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/nxdn/channel/SACCH.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
|
||||
using namespace nxdn;
|
||||
using namespace nxdn::defines;
|
||||
using namespace nxdn::channel;
|
||||
|
||||
TEST_CASE("SACCH encodes and decodes idle pattern", "[nxdn][sacch]") {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
SACCH sacch;
|
||||
sacch.setData(SACCH_IDLE);
|
||||
sacch.setRAN(1U);
|
||||
sacch.setStructure(ChStructure::SR_SINGLE);
|
||||
|
||||
sacch.encode(data);
|
||||
|
||||
// Decode and verify
|
||||
SACCH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRAN() == 1U);
|
||||
REQUIRE(decoded.getStructure() == ChStructure::SR_SINGLE);
|
||||
|
||||
// Verify data matches
|
||||
uint8_t decodedData[3U];
|
||||
decoded.getData(decodedData);
|
||||
REQUIRE(::memcmp(decodedData, SACCH_IDLE, 3U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("SACCH preserves all RAN values", "[nxdn][sacch]") {
|
||||
for (uint8_t ran = 0U; ran < 64U; ran++) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
SACCH sacch;
|
||||
sacch.setData(SACCH_IDLE);
|
||||
sacch.setRAN(ran);
|
||||
sacch.setStructure(ChStructure::SR_SINGLE);
|
||||
|
||||
sacch.encode(data);
|
||||
|
||||
SACCH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getRAN() == ran);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("SACCH preserves all ChStructure values", "[nxdn][sacch]") {
|
||||
const ChStructure::E structures[] = {
|
||||
ChStructure::SR_SINGLE,
|
||||
ChStructure::SR_1_4,
|
||||
ChStructure::SR_2_4,
|
||||
ChStructure::SR_3_4,
|
||||
ChStructure::SR_RCCH_SINGLE
|
||||
};
|
||||
|
||||
for (auto structure : structures) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
SACCH sacch;
|
||||
sacch.setData(SACCH_IDLE);
|
||||
sacch.setRAN(1U);
|
||||
sacch.setStructure(structure);
|
||||
|
||||
sacch.encode(data);
|
||||
|
||||
SACCH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getStructure() == structure);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("SACCH copy constructor preserves all fields", "[nxdn][sacch]") {
|
||||
SACCH original;
|
||||
original.setData(SACCH_IDLE);
|
||||
original.setRAN(5U);
|
||||
original.setStructure(ChStructure::SR_1_4);
|
||||
|
||||
SACCH copy(original);
|
||||
REQUIRE(copy.getRAN() == original.getRAN());
|
||||
REQUIRE(copy.getStructure() == original.getStructure());
|
||||
|
||||
// initialize buffers to zero since getData() only writes 18 bits (NXDN_SACCH_LENGTH_BITS - 8)
|
||||
uint8_t originalData[3U], copyData[3U];
|
||||
::memset(originalData, 0x00U, 3U);
|
||||
::memset(copyData, 0x00U, 3U);
|
||||
original.getData(originalData);
|
||||
Utils::dump(2U, "originalData", originalData, 3U);
|
||||
copy.getData(copyData);
|
||||
Utils::dump(2U, "copyData", copyData, 3U);
|
||||
REQUIRE(::memcmp(originalData, copyData, 3U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("SACCH assignment operator preserves all fields", "[nxdn][sacch]") {
|
||||
SACCH original;
|
||||
const uint8_t testData[] = {0x12, 0x34, 0x56};
|
||||
original.setData(testData);
|
||||
original.setRAN(10U);
|
||||
original.setStructure(ChStructure::SR_2_4);
|
||||
|
||||
SACCH assigned;
|
||||
assigned = original;
|
||||
REQUIRE(assigned.getRAN() == original.getRAN());
|
||||
REQUIRE(assigned.getStructure() == original.getStructure());
|
||||
|
||||
// initialize buffers to zero since getData() only writes 18 bits (NXDN_SACCH_LENGTH_BITS - 8)
|
||||
uint8_t originalData[3U], assignedData[3U];
|
||||
::memset(originalData, 0x00U, 3U);
|
||||
::memset(assignedData, 0x00U, 3U);
|
||||
original.getData(originalData);
|
||||
Utils::dump(2U, "originalData", originalData, 3U);
|
||||
assigned.getData(assignedData);
|
||||
Utils::dump(2U, "assignedData", assignedData, 3U);
|
||||
REQUIRE(::memcmp(originalData, assignedData, 3U) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("SACCH handles multi-part structures", "[nxdn][sacch]") {
|
||||
// Test multi-part SACCH structures (SR_1_4, SR_2_4, etc.)
|
||||
const ChStructure::E multiPart[] = {
|
||||
ChStructure::SR_1_4,
|
||||
ChStructure::SR_2_4,
|
||||
ChStructure::SR_3_4
|
||||
};
|
||||
|
||||
for (auto structure : multiPart) {
|
||||
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
SACCH sacch;
|
||||
const uint8_t testData[] = {0xA5, 0x5A, 0xC0};
|
||||
sacch.setData(testData);
|
||||
sacch.setRAN(7U);
|
||||
sacch.setStructure(structure);
|
||||
|
||||
sacch.encode(data);
|
||||
|
||||
SACCH decoded;
|
||||
REQUIRE(decoded.decode(data));
|
||||
REQUIRE(decoded.getStructure() == structure);
|
||||
REQUIRE(decoded.getRAN() == 7U);
|
||||
|
||||
uint8_t decodedData[3U];
|
||||
decoded.getData(decodedData);
|
||||
Utils::dump(2U, "decodedData", decodedData, 3U);
|
||||
REQUIRE(::memcmp(decodedData, testData, 3U) == 0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "host/Defines.h"
|
||||
#include "common/p25/lc/tdulc/LC_GROUP.h"
|
||||
#include "common/p25/lc/tdulc/LC_PRIVATE.h"
|
||||
#include "common/p25/P25Defines.h"
|
||||
#include "common/edac/Golay24128.h"
|
||||
#include "common/edac/RS634717.h"
|
||||
|
||||
using namespace p25;
|
||||
using namespace p25::defines;
|
||||
using namespace p25::lc;
|
||||
using namespace p25::lc::tdulc;
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
TEST_CASE("TDULC", "[p25][tdulc]") {
|
||||
SECTION("Constants_Valid") {
|
||||
// Verify TDULC length constants
|
||||
REQUIRE(P25_TDULC_LENGTH_BYTES == 18); // Total length with RS FEC
|
||||
REQUIRE(P25_TDULC_PAYLOAD_LENGTH_BYTES == 8); // Payload only
|
||||
REQUIRE(P25_TDULC_FEC_LENGTH_BYTES == 36); // After Golay encoding
|
||||
REQUIRE(P25_TDULC_FRAME_LENGTH_BYTES == 54); // Full frame with preamble
|
||||
}
|
||||
|
||||
SECTION("Golay_Encode_Decode") {
|
||||
// Test Golay (24,12,8) FEC encoding/decoding
|
||||
uint8_t input[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(input, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Set test pattern
|
||||
input[0] = 0x12;
|
||||
input[1] = 0x34;
|
||||
input[2] = 0x56;
|
||||
input[3] = 0x78;
|
||||
|
||||
// Encode with Golay
|
||||
uint8_t encoded[P25_TDULC_FEC_LENGTH_BYTES + 1];
|
||||
::memset(encoded, 0x00, P25_TDULC_FEC_LENGTH_BYTES + 1);
|
||||
edac::Golay24128::encode24128(encoded, input, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Decode with Golay
|
||||
uint8_t decoded[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(decoded, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
edac::Golay24128::decode24128(decoded, encoded, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Verify round-trip
|
||||
for (uint32_t i = 0; i < P25_TDULC_LENGTH_BYTES; i++) {
|
||||
REQUIRE(decoded[i] == input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("RS_241213_Encode_Decode") {
|
||||
// Test RS (24,12,13) FEC encoding/decoding
|
||||
edac::RS634717 rs;
|
||||
|
||||
uint8_t input[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(input, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Set test pattern in first 12 bytes (data portion)
|
||||
for (uint32_t i = 0; i < 12; i++) {
|
||||
input[i] = (uint8_t)(i * 0x11);
|
||||
}
|
||||
|
||||
// Encode RS (adds 6 parity bytes)
|
||||
rs.encode241213(input);
|
||||
|
||||
// Decode RS
|
||||
bool result = rs.decode241213(input);
|
||||
|
||||
REQUIRE(result == true);
|
||||
}
|
||||
|
||||
SECTION("LCO_Values") {
|
||||
// Test various LCO values (6 bits)
|
||||
uint8_t lcoValues[] = { 0x00, 0x01, 0x02, 0x03, 0x20, 0x3F };
|
||||
|
||||
for (auto lco : lcoValues) {
|
||||
LC_GROUP tdulc;
|
||||
tdulc.setLCO(lco & 0x3F); // Mask to 6 bits
|
||||
|
||||
REQUIRE(tdulc.getLCO() == (lco & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Emergency_Flag") {
|
||||
// Test emergency flag
|
||||
LC_GROUP tdulc;
|
||||
|
||||
tdulc.setEmergency(false);
|
||||
REQUIRE(tdulc.getEmergency() == false);
|
||||
|
||||
tdulc.setEmergency(true);
|
||||
REQUIRE(tdulc.getEmergency() == true);
|
||||
}
|
||||
|
||||
SECTION("Encrypted_Flag") {
|
||||
// Test encrypted flag
|
||||
LC_GROUP tdulc;
|
||||
|
||||
tdulc.setEncrypted(false);
|
||||
REQUIRE(tdulc.getEncrypted() == false);
|
||||
|
||||
tdulc.setEncrypted(true);
|
||||
REQUIRE(tdulc.getEncrypted() == true);
|
||||
}
|
||||
|
||||
SECTION("Priority_Values") {
|
||||
// Test priority values (3 bits: 0-7)
|
||||
for (uint8_t priority = 0; priority <= 7; priority++) {
|
||||
LC_GROUP tdulc;
|
||||
tdulc.setPriority(priority);
|
||||
|
||||
REQUIRE(tdulc.getPriority() == priority);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Group_Flag") {
|
||||
// Test group flag
|
||||
LC_GROUP groupTdulc;
|
||||
groupTdulc.setGroup(true);
|
||||
REQUIRE(groupTdulc.getGroup() == true);
|
||||
|
||||
LC_PRIVATE privateTdulc;
|
||||
privateTdulc.setGroup(false);
|
||||
REQUIRE(privateTdulc.getGroup() == false);
|
||||
}
|
||||
|
||||
SECTION("SrcId_Values") {
|
||||
// Test source ID values (24 bits)
|
||||
uint32_t srcIds[] = { 0x000000, 0x000001, 0x123456, 0xFFFFFE, 0xFFFFFF };
|
||||
|
||||
for (auto srcId : srcIds) {
|
||||
LC_GROUP tdulc;
|
||||
tdulc.setSrcId(srcId & 0xFFFFFF); // Mask to 24 bits
|
||||
|
||||
REQUIRE(tdulc.getSrcId() == (srcId & 0xFFFFFF));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("DstId_Values") {
|
||||
// Test destination ID values (16 bits for group)
|
||||
uint32_t dstIds[] = { 0x0000, 0x0001, 0x1234, 0xFFFE, 0xFFFF };
|
||||
|
||||
for (auto dstId : dstIds) {
|
||||
LC_GROUP tdulc;
|
||||
tdulc.setDstId(dstId & 0xFFFF); // Mask to 16 bits
|
||||
|
||||
REQUIRE(tdulc.getDstId() == (dstId & 0xFFFF));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("MfgId_Values") {
|
||||
// Test manufacturer ID values
|
||||
uint8_t mfgIds[] = { 0x00, 0x01, 0x90, 0xFF };
|
||||
|
||||
for (auto mfgId : mfgIds) {
|
||||
LC_GROUP tdulc;
|
||||
tdulc.setMFId(mfgId);
|
||||
|
||||
REQUIRE(tdulc.getMFId() == mfgId);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("AllZeros_Pattern") {
|
||||
// Test all-zeros pattern
|
||||
LC_GROUP tdulc;
|
||||
|
||||
tdulc.setLCO(0x00);
|
||||
tdulc.setMFId(0x00);
|
||||
tdulc.setSrcId(0x000000);
|
||||
tdulc.setDstId(0x0000);
|
||||
tdulc.setEmergency(false);
|
||||
tdulc.setEncrypted(false);
|
||||
tdulc.setPriority(0);
|
||||
|
||||
REQUIRE(tdulc.getLCO() == 0x00);
|
||||
REQUIRE(tdulc.getMFId() == 0x00);
|
||||
REQUIRE(tdulc.getSrcId() == 0x000000);
|
||||
REQUIRE(tdulc.getDstId() == 0x0000);
|
||||
REQUIRE(tdulc.getEmergency() == false);
|
||||
REQUIRE(tdulc.getEncrypted() == false);
|
||||
REQUIRE(tdulc.getPriority() == 0);
|
||||
}
|
||||
|
||||
SECTION("MaxValues_Pattern") {
|
||||
// Test maximum values pattern
|
||||
LC_GROUP tdulc;
|
||||
|
||||
tdulc.setLCO(0x3F); // 6 bits max
|
||||
tdulc.setMFId(0xFF); // 8 bits max
|
||||
tdulc.setSrcId(0xFFFFFF); // 24 bits max
|
||||
tdulc.setDstId(0xFFFF); // 16 bits max
|
||||
tdulc.setEmergency(true);
|
||||
tdulc.setEncrypted(true);
|
||||
tdulc.setPriority(7); // 3 bits max
|
||||
|
||||
REQUIRE(tdulc.getLCO() == 0x3F);
|
||||
REQUIRE(tdulc.getMFId() == 0xFF);
|
||||
REQUIRE(tdulc.getSrcId() == 0xFFFFFF);
|
||||
REQUIRE(tdulc.getDstId() == 0xFFFF);
|
||||
REQUIRE(tdulc.getEmergency() == true);
|
||||
REQUIRE(tdulc.getEncrypted() == true);
|
||||
REQUIRE(tdulc.getPriority() == 7);
|
||||
}
|
||||
|
||||
SECTION("Group_Copy_Constructor") {
|
||||
// Test copy constructor for LC_GROUP
|
||||
LC_GROUP tdulc1;
|
||||
|
||||
tdulc1.setLCO(0x00);
|
||||
tdulc1.setMFId(0x90);
|
||||
tdulc1.setSrcId(0x123456);
|
||||
tdulc1.setDstId(0xABCD);
|
||||
tdulc1.setEmergency(true);
|
||||
tdulc1.setEncrypted(false);
|
||||
tdulc1.setPriority(5);
|
||||
|
||||
LC_GROUP tdulc2(tdulc1);
|
||||
|
||||
REQUIRE(tdulc2.getLCO() == tdulc1.getLCO());
|
||||
REQUIRE(tdulc2.getMFId() == tdulc1.getMFId());
|
||||
REQUIRE(tdulc2.getSrcId() == tdulc1.getSrcId());
|
||||
REQUIRE(tdulc2.getDstId() == tdulc1.getDstId());
|
||||
REQUIRE(tdulc2.getEmergency() == tdulc1.getEmergency());
|
||||
REQUIRE(tdulc2.getEncrypted() == tdulc1.getEncrypted());
|
||||
REQUIRE(tdulc2.getPriority() == tdulc1.getPriority());
|
||||
}
|
||||
|
||||
SECTION("Private_Copy_Constructor") {
|
||||
// Test copy constructor for LC_PRIVATE
|
||||
LC_PRIVATE tdulc1;
|
||||
|
||||
tdulc1.setLCO(0x03);
|
||||
tdulc1.setMFId(0x00);
|
||||
tdulc1.setSrcId(0xABCDEF);
|
||||
tdulc1.setDstId(0x123456);
|
||||
tdulc1.setEmergency(false);
|
||||
tdulc1.setEncrypted(true);
|
||||
tdulc1.setPriority(3);
|
||||
|
||||
LC_PRIVATE tdulc2(tdulc1);
|
||||
|
||||
REQUIRE(tdulc2.getLCO() == tdulc1.getLCO());
|
||||
REQUIRE(tdulc2.getMFId() == tdulc1.getMFId());
|
||||
REQUIRE(tdulc2.getSrcId() == tdulc1.getSrcId());
|
||||
REQUIRE(tdulc2.getDstId() == tdulc1.getDstId());
|
||||
REQUIRE(tdulc2.getEmergency() == tdulc1.getEmergency());
|
||||
REQUIRE(tdulc2.getEncrypted() == tdulc1.getEncrypted());
|
||||
REQUIRE(tdulc2.getPriority() == tdulc1.getPriority());
|
||||
}
|
||||
|
||||
SECTION("Golay_ErrorCorrection") {
|
||||
// Test Golay error correction capability
|
||||
uint8_t input[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(input, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Set known pattern
|
||||
input[0] = 0xAA;
|
||||
input[1] = 0x55;
|
||||
input[2] = 0xF0;
|
||||
input[3] = 0x0F;
|
||||
|
||||
// Encode
|
||||
uint8_t encoded[P25_TDULC_FEC_LENGTH_BYTES + 1];
|
||||
::memset(encoded, 0x00, P25_TDULC_FEC_LENGTH_BYTES + 1);
|
||||
edac::Golay24128::encode24128(encoded, input, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Introduce single bit error (Golay can correct up to 3 bit errors)
|
||||
encoded[5] ^= 0x01;
|
||||
|
||||
// Decode (should correct the error)
|
||||
uint8_t decoded[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(decoded, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
edac::Golay24128::decode24128(decoded, encoded, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Verify correction
|
||||
REQUIRE(decoded[0] == input[0]);
|
||||
REQUIRE(decoded[1] == input[1]);
|
||||
REQUIRE(decoded[2] == input[2]);
|
||||
REQUIRE(decoded[3] == input[3]);
|
||||
}
|
||||
|
||||
SECTION("RS_ErrorCorrection") {
|
||||
// Test RS error correction capability
|
||||
edac::RS634717 rs;
|
||||
|
||||
uint8_t data[P25_TDULC_LENGTH_BYTES];
|
||||
::memset(data, 0x00, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Set known data pattern in first 12 bytes
|
||||
for (uint32_t i = 0; i < 12; i++) {
|
||||
data[i] = (uint8_t)(0xAA - i);
|
||||
}
|
||||
|
||||
// Encode RS
|
||||
rs.encode241213(data);
|
||||
|
||||
// Save original
|
||||
uint8_t original[P25_TDULC_LENGTH_BYTES];
|
||||
::memcpy(original, data, P25_TDULC_LENGTH_BYTES);
|
||||
|
||||
// Introduce errors (RS can correct up to 6 byte errors with (24,12,13))
|
||||
data[2] ^= 0xFF;
|
||||
data[5] ^= 0xFF;
|
||||
|
||||
// Decode (should correct the errors)
|
||||
bool result = rs.decode241213(data);
|
||||
|
||||
REQUIRE(result == true);
|
||||
|
||||
// Verify correction
|
||||
for (uint32_t i = 0; i < 12; i++) {
|
||||
REQUIRE(data[i] == original[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,329 @@
|
||||
// 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) 2025 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "host/Defines.h"
|
||||
#include "common/p25/lc/tsbk/OSP_SCCB.h"
|
||||
#include "common/p25/lc/tsbk/OSP_TSBK_RAW.h"
|
||||
#include "common/p25/P25Defines.h"
|
||||
#include "common/edac/CRC.h"
|
||||
#include "common/Log.h"
|
||||
#include "common/Utils.h"
|
||||
|
||||
using namespace p25;
|
||||
using namespace p25::defines;
|
||||
using namespace p25::lc;
|
||||
using namespace p25::lc::tsbk;
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
TEST_CASE("TSBK", "[p25][tsbk]") {
|
||||
SECTION("Constants_Valid") {
|
||||
// Verify TSBK length constants
|
||||
REQUIRE(P25_TSBK_LENGTH_BYTES == 12);
|
||||
REQUIRE(P25_TSBK_FEC_LENGTH_BYTES == 25);
|
||||
REQUIRE(P25_TSBK_FEC_LENGTH_BITS == (P25_TSBK_FEC_LENGTH_BYTES * 8 - 4)); // 196 bits (Trellis)
|
||||
}
|
||||
|
||||
SECTION("RawTSBK_Encode_Decode_NoTrellis") {
|
||||
g_logDisplayLevel = 1U;
|
||||
|
||||
// Test raw TSBK encoding/decoding without Trellis
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
// Create a test TSBK payload
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Set LCO (Link Control Opcode)
|
||||
testTSBK[0] = 0x34; // Example LCO (OSP_SCCB)
|
||||
testTSBK[1] = 0x00; // Mfg ID (standard)
|
||||
|
||||
// Set some payload data
|
||||
for (uint32_t i = 2; i < P25_TSBK_LENGTH_BYTES - 2; i++) {
|
||||
testTSBK[i] = (uint8_t)(i * 0x11);
|
||||
}
|
||||
|
||||
// Add CRC
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
Utils::dump(2U, "testTSBK", testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Set the TSBK
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
// Encode (raw, no Trellis)
|
||||
uint8_t encoded[P25_TSBK_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
Utils::dump(2U, "encoded", encoded, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Verify encoded matches input
|
||||
for (uint32_t i = 0; i < P25_TSBK_LENGTH_BYTES - 2; i++) {
|
||||
REQUIRE(encoded[i] == testTSBK[i]);
|
||||
}
|
||||
|
||||
// Decode back
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
bool result = tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(tsbk2.getLCO() == (testTSBK[0] & 0x3F));
|
||||
REQUIRE(tsbk2.getMFId() == testTSBK[1]);
|
||||
}
|
||||
|
||||
SECTION("RawTSBK_Encode_Decode_WithTrellis") {
|
||||
g_logDisplayLevel = 1U;
|
||||
|
||||
// Test raw TSBK encoding/decoding with Trellis FEC
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
testTSBK[0] = 0x34; // LCO
|
||||
testTSBK[1] = 0x00; // Mfg ID
|
||||
|
||||
// Set payload
|
||||
testTSBK[2] = 0xAA;
|
||||
testTSBK[3] = 0x55;
|
||||
testTSBK[4] = 0xF0;
|
||||
testTSBK[5] = 0x0F;
|
||||
testTSBK[6] = 0xCC;
|
||||
testTSBK[7] = 0x33;
|
||||
testTSBK[8] = 0x12;
|
||||
testTSBK[9] = 0x34;
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
Utils::dump(2U, "testTSBK", testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
// Encode with Trellis
|
||||
uint8_t encoded[P25_TSDU_FRAME_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded);
|
||||
|
||||
// Decode with Trellis
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
bool result = tsbk2.decode(encoded);
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(tsbk2.getLCO() == (testTSBK[0] & 0x3F));
|
||||
REQUIRE(tsbk2.getMFId() == testTSBK[1]);
|
||||
}
|
||||
|
||||
SECTION("LastBlock_Flag") {
|
||||
// Test Last Block Marker flag
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Set Last Block flag (bit 7 of byte 0)
|
||||
testTSBK[0] = 0x80 | 0x34; // Last Block + LCO
|
||||
testTSBK[1] = 0x00;
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(tsbk2.getLastBlock() == true);
|
||||
REQUIRE(tsbk2.getLCO() == 0x34);
|
||||
}
|
||||
|
||||
SECTION("MfgId_Preservation") {
|
||||
// Test Manufacturer ID preservation
|
||||
uint8_t mfgIds[] = { 0x00, 0x01, 0x90, 0xFF };
|
||||
|
||||
for (auto mfgId : mfgIds) {
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
testTSBK[0] = 0x34; // LCO
|
||||
testTSBK[1] = mfgId;
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(tsbk2.getMFId() == mfgId);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("CRC_CCITT16_Validation") {
|
||||
// Test CRC-CCITT16 validation
|
||||
OSP_TSBK_RAW tsbk;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
testTSBK[0] = 0x34;
|
||||
testTSBK[1] = 0x00;
|
||||
testTSBK[2] = 0xAB;
|
||||
testTSBK[3] = 0xCD;
|
||||
|
||||
// Add valid CRC
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Verify CRC is valid
|
||||
bool crcValid = edac::CRC::checkCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
REQUIRE(crcValid == true);
|
||||
|
||||
// Corrupt the CRC
|
||||
testTSBK[P25_TSBK_LENGTH_BYTES - 1] ^= 0xFF;
|
||||
|
||||
// Verify CRC is now invalid
|
||||
crcValid = edac::CRC::checkCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
REQUIRE(crcValid == false);
|
||||
}
|
||||
|
||||
SECTION("Payload_RoundTrip") {
|
||||
// Test payload data round-trip
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
testTSBK[0] = 0x34;
|
||||
testTSBK[1] = 0x00;
|
||||
|
||||
// Payload is bytes 2-9 (8 bytes)
|
||||
uint8_t expectedPayload[8] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF };
|
||||
::memcpy(testTSBK + 2, expectedPayload, 8);
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
// Encode and decode
|
||||
uint8_t encoded[P25_TSBK_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
tsbk2.decode(encoded, true);
|
||||
|
||||
// Get decoded raw data and verify payload
|
||||
uint8_t* decoded = tsbk2.getDecodedRaw();
|
||||
REQUIRE(decoded != nullptr);
|
||||
|
||||
for (uint32_t i = 0; i < 8; i++) {
|
||||
REQUIRE(decoded[i + 2] == expectedPayload[i]);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("AllZeros_Pattern") {
|
||||
// Test all-zeros pattern
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_FEC_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
bool result = tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(tsbk2.getLCO() == 0x00);
|
||||
REQUIRE(tsbk2.getMFId() == 0x00);
|
||||
}
|
||||
|
||||
SECTION("AllOnes_Pattern") {
|
||||
// Test all-ones pattern
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0xFF, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
// Keep LCO valid (only 6 bits)
|
||||
testTSBK[0] = 0xFF; // Last Block + all LCO bits set
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_FEC_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
bool result = tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(tsbk2.getLCO() == 0x3F); // Only 6 bits
|
||||
REQUIRE(tsbk2.getLastBlock() == true);
|
||||
}
|
||||
|
||||
SECTION("Alternating_Pattern") {
|
||||
// Test alternating bit pattern
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
for (uint32_t i = 0; i < P25_TSBK_LENGTH_BYTES; i++) {
|
||||
testTSBK[i] = (i % 2 == 0) ? 0xAA : 0x55;
|
||||
}
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_FEC_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
bool result = tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(result == true);
|
||||
}
|
||||
|
||||
SECTION("LCO_Values") {
|
||||
// Test various LCO values (6 bits)
|
||||
uint8_t lcoValues[] = { 0x00, 0x01, 0x0F, 0x20, 0x34, 0x3F };
|
||||
|
||||
for (auto lco : lcoValues) {
|
||||
OSP_TSBK_RAW tsbk1;
|
||||
|
||||
uint8_t testTSBK[P25_TSBK_LENGTH_BYTES];
|
||||
::memset(testTSBK, 0x00, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
testTSBK[0] = lco & 0x3F; // Mask to 6 bits
|
||||
testTSBK[1] = 0x00;
|
||||
|
||||
edac::CRC::addCCITT162(testTSBK, P25_TSBK_LENGTH_BYTES);
|
||||
|
||||
tsbk1.setTSBK(testTSBK);
|
||||
|
||||
uint8_t encoded[P25_TSBK_LENGTH_BYTES];
|
||||
tsbk1.encode(encoded, true, true);
|
||||
|
||||
OSP_TSBK_RAW tsbk2;
|
||||
tsbk2.decode(encoded, true);
|
||||
|
||||
REQUIRE(tsbk2.getLCO() == (lco & 0x3F));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue