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.
255 lines
7.8 KiB
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));
|
|
}
|
|
}
|
|
}
|