You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dvmhost/tests/nxdn/LICH_Tests.cpp

255 lines
7.8 KiB

// 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);
}
TEST_CASE("LICH rejects invalid parity", "[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::DATA_NORMAL);
lich.setOutbound(true);
lich.encode(data);
WRITE_BIT(data, NXDN_FSW_LENGTH_BITS, !READ_BIT(data, NXDN_FSW_LENGTH_BITS));
LICH decoded;
REQUIRE_FALSE(decoded.decode(data));
}
TEST_CASE("LICH uses even parity over every RFCT and FCT combination", "[nxdn][lich][golden]") {
for (uint8_t rfct = 0U; rfct < 4U; rfct++) {
for (uint8_t fct = 0U; fct < 4U; fct++) {
uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U] = {};
LICH lich;
lich.setRFCT(static_cast<RFChannelType::E>(rfct));
lich.setFCT(static_cast<FuncChannelType::E>(fct));
lich.setOption(ChOption::DATA_NORMAL);
lich.setOutbound(false);
lich.encode(data);
uint8_t encodedLICH[1U] = {};
for (uint32_t bit = 0U; bit < 8U; bit++) {
const uint32_t offset = NXDN_FSW_LENGTH_BITS + bit * 2U;
WRITE_BIT(encodedLICH, bit, READ_BIT(data, offset));
REQUIRE(READ_BIT(data, offset + 1U));
}
const uint8_t highNibble = static_cast<uint8_t>((rfct << 2U) | fct);
const bool expectedParity = ((highNibble >> 3U) ^ (highNibble >> 2U) ^
(highNibble >> 1U) ^ highNibble) & 0x01U;
REQUIRE((encodedLICH[0U] & 0x01U) == (expectedParity ? 0x01U : 0x00U));
}
}
}

Powered by TurnKey Linux.