correct DMR PDU handling;

r05a06_dev
Bryan Biedenkapp 5 days ago
parent a27e87ede1
commit 5091225a9f

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2018-2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2018-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -84,8 +84,11 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header)
else if (m_dataType == DataType::RATE_12_DATA) {
m_bptc.decode(data, buffer);
}
else if (m_dataType == DataType::RATE_1_DATA) {
decodeRate1(data, buffer);
}
else {
LogError(LOG_DMR, "DataBlock::decode(), cowardly refusing to decode confirmed full-rate (rate 1) data");
LogError(LOG_DMR, "DataBlock::decode(), invalid confirmed dataType = $%02X", m_dataType);
return false;
}
@ -95,6 +98,7 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header)
m_serialNo = (buffer[0] & 0xFEU) >> 1; // Confirmed Data Serial No.
uint16_t crc = ((buffer[0] & 0x01U) << 8) + buffer[1]; // CRC-9 Check Sum
crc ^= crcMask(m_dataType);
::memset(m_data, 0x00U, DMR_PDU_UNCODED_LENGTH_BYTES);
if (m_dataType == DataType::RATE_34_DATA)
@ -150,6 +154,7 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header)
if ((crc ^ calculated) != 0) {
LogWarning(LOG_DMR, "DMR, dataType = $%02X, invalid crc = $%04X != $%04X (computed)", m_dataType, crc, calculated);
return false;
}
#if DEBUG_DMR_PDU_DATA
@ -174,9 +179,12 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header)
else if (m_dataType == DataType::RATE_12_DATA) {
m_bptc.decode(data, buffer);
}
else if (m_dataType == DataType::RATE_1_DATA) {
decodeRate1(data, buffer);
}
else {
::memcpy(buffer, data, DMR_PDU_UNCODED_LENGTH_BYTES);
return true; // never do any further processing for uncoded
LogError(LOG_DMR, "DataBlock::decode(), invalid unconfirmed dataType = $%02X", m_dataType);
return false;
}
::memset(m_data, 0x00U, DMR_PDU_UNCODED_LENGTH_BYTES);
@ -184,6 +192,8 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header)
::memcpy(m_data, buffer, DMR_PDU_THREEQUARTER_LENGTH_BYTES); // Payload Data
else if (m_dataType == DataType::RATE_12_DATA)
::memcpy(m_data, buffer, DMR_PDU_HALFRATE_LENGTH_BYTES); // Payload Data
else if (m_dataType == DataType::RATE_1_DATA)
::memcpy(m_data, buffer, DMR_PDU_UNCODED_LENGTH_BYTES); // Payload Data
else {
LogError(LOG_DMR, "DataBlock::decode(), failed to decode block, invalid dataType = $%02X", m_dataType);
return false;
@ -229,6 +239,7 @@ void DataBlock::encode(uint8_t* data)
uint16_t crc = edac::CRC::createCRC9(crcBuffer, 135U);
crc = ~crc & 0x1FFU;
crc ^= crcMask(m_dataType);
buffer[0U] = buffer[0U] + ((crc >> 8) & 0x01U); // CRC-9 Check Sum (b8)
buffer[1U] = (crc & 0xFFU); // CRC-9 Check Sum (b0 - b7)
@ -259,12 +270,11 @@ void DataBlock::encode(uint8_t* data)
uint16_t crc = edac::CRC::createCRC9(crcBuffer, 87U);
crc = ~crc & 0x1FFU;
crc ^= crcMask(m_dataType);
buffer[0U] = buffer[0U] + ((crc >> 8) & 0x01U); // CRC-9 Check Sum (b8)
buffer[1U] = (crc & 0xFFU); // CRC-9 Check Sum (b0 - b7)
::memcpy(buffer, m_data, DMR_PDU_HALFRATE_LENGTH_BYTES);
#if DEBUG_DMR_PDU_DATA
Utils::dump(1U, "DMR, DataBlock::encode(), Confirmed 1/2 Rate PDU Data Block", buffer, DMR_PDU_HALFRATE_LENGTH_BYTES);
#endif
@ -291,17 +301,16 @@ void DataBlock::encode(uint8_t* data)
uint16_t crc = edac::CRC::createCRC9(crcBuffer, 183U);
crc = ~crc & 0x1FFU;
crc ^= crcMask(m_dataType);
buffer[0U] = buffer[0U] + ((crc >> 8) & 0x01U); // CRC-9 Check Sum (b8)
buffer[1U] = (crc & 0xFFU); // CRC-9 Check Sum (b0 - b7)
::memcpy(buffer, m_data, DMR_PDU_UNCODED_LENGTH_BYTES);
#if DEBUG_DMR_PDU_DATA
Utils::dump(1U, "DMR, DataBlock::encode(), Confirmed 1 Rate PDU Data Block", buffer, DMR_PDU_UNCODED_LENGTH_BYTES);
#endif
::memcpy(data, buffer, DMR_PDU_UNCODED_LENGTH_BYTES);
encodeRate1(buffer, data);
}
}
else if (m_DPF == DPF::UNCONFIRMED_DATA || m_DPF == DPF::RESPONSE || m_DPF == DPF::DEFINED_RAW ||
@ -336,7 +345,7 @@ void DataBlock::encode(uint8_t* data)
::memcpy(buffer, m_data, DMR_PDU_UNCODED_LENGTH_BYTES);
::memcpy(data, buffer, DMR_PDU_UNCODED_LENGTH_BYTES);
encodeRate1(buffer, data);
#if DEBUG_DMR_PDU_DATA
Utils::dump(1U, "DMR, DataBlock::encode(), Unconfirmed 1 Rate PDU Data Block", buffer, DMR_PDU_UNCODED_LENGTH_BYTES);
@ -454,6 +463,42 @@ uint32_t DataBlock::getData(uint8_t* buffer) const
// Private Class Members
// ---------------------------------------------------------------------------
/* */
uint16_t DataBlock::crcMask(const DataType::E dataType) const
{
switch (dataType) {
case DataType::RATE_12_DATA:
return 0x0F0U;
case DataType::RATE_34_DATA:
return 0x1FFU;
case DataType::RATE_1_DATA:
return 0x10FU;
default:
return 0U;
}
}
/* Decodes a rate 1 encoded data block. */
void DataBlock::decodeRate1(const uint8_t* data, uint8_t* payload)
{
::memcpy(payload, data, 12U);
::memcpy(payload + 12U, data + 21U, 12U);
}
/* Encodes a data block using rate 1 encoding. */
void DataBlock::encodeRate1(const uint8_t* payload, uint8_t* data)
{
::memcpy(data, payload, 12U);
::memcpy(data + 21U, payload + 12U, 12U);
// annex B.2.5 inserts four zero padding bits between the two halves
data[12U] &= 0x3FU;
data[20U] &= 0xFCU;
}
/* Internal helper to copy the the class. */
void DataBlock::copy(const DataBlock& data)

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024,2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -123,6 +123,26 @@ namespace dmr
uint8_t* m_data;
/**
* @brief Gets the CRC mask for the given data type.
* @param dataType DMR data type.
* @returns uint16_t CRC mask.
*/
uint16_t crcMask(const defines::DataType::E dataType) const;
/**
* @brief Decodes a rate 1 encoded data block.
* @param data Pointer to the encoded data.
* @param payload Pointer to the buffer to store the decoded payload.
*/
void decodeRate1(const uint8_t* data, uint8_t* payload);
/**
* @brief Encodes a data block using rate 1 encoding.
* @param payload Pointer to the buffer containing the payload to encode.
* @param data Pointer to the buffer to store the encoded data.
*/
void encodeRate1(const uint8_t* payload, uint8_t* data);
/**
* @brief Internal helper to copy the class.
*/

@ -6,7 +6,7 @@
*
* Copyright (C) 2012 Ian Wraith
* Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX
* Copyright (C) 2021,2023,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2021,2023,2024,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -326,7 +326,7 @@ void DataHeader::encode(uint8_t* data)
(m_blocksToFollow & 0x0FU); // Blocks To Follow LSB
m_data[8U] = (m_F ? 0x01U : 0x00U) + // Full Message Flag
(m_S ? 0x02U : 0x00U) + // Synchronize Flag
((m_dataFormat & 0xFCU) << 2); // Defined Data Format
((m_dataFormat & 0x3FU) << 2); // Defined Data Format
m_data[9U] = m_padLength; // Bit Padding
#if DEBUG_DMR_PDU_DATA
Utils::dump(1U, "DMR, DataHeader::decode(), Defined Short Data Header", m_data, DMR_LC_HEADER_LENGTH_BYTES);

@ -0,0 +1,194 @@
// 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 <array>
#include <cstring>
#include "common/Defines.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/data/DataBlock.h"
#include "common/dmr/data/DataHeader.h"
#include "common/edac/BPTC19696.h"
#include "common/edac/CRC.h"
#include "common/edac/Trellis.h"
using namespace dmr::defines;
using namespace dmr::data;
namespace {
/**
* @brief Returns the CRC mask for the given data type.
* @param type The data type for which to retrieve the CRC mask.
* @return constexpr uint16_t
*/
constexpr uint16_t crcMask(DataType::E type)
{
return type == DataType::RATE_12_DATA ? 0x0F0U :
type == DataType::RATE_34_DATA ? 0x1FFU : 0x10FU;
}
/**
* @brief Returns the payload length for the given data type and confirmation status.
* @param type The data type for which to retrieve the payload length.
* @param confirmed Whether the data is confirmed.
* @return constexpr uint32_t The payload length in bytes.
*/
constexpr uint32_t payloadLength(DataType::E type, bool confirmed)
{
if (type == DataType::RATE_12_DATA)
return confirmed ? DMR_PDU_CONFIRMED_HR_DATA_LENGTH_BYTES : DMR_PDU_HALFRATE_LENGTH_BYTES;
if (type == DataType::RATE_34_DATA)
return confirmed ? DMR_PDU_CONFIRMED_TQ_DATA_LENGTH_BYTES : DMR_PDU_THREEQUARTER_LENGTH_BYTES;
return confirmed ? DMR_PDU_CONFIRMED_UNCODED_DATA_LENGTH_BYTES : DMR_PDU_UNCODED_LENGTH_BYTES;
}
/**
* @brief Extracts a data block from the given frame based on the data type.
* @param type The data type of the block to extract.
* @param frame The input frame containing the encoded data.
* @param block The output buffer to store the extracted data block.
*/
void extractBlock(DataType::E type, const uint8_t* frame, uint8_t* block)
{
if (type == DataType::RATE_12_DATA) {
edac::BPTC19696().decode(frame, block);
} else if (type == DataType::RATE_34_DATA) {
REQUIRE(edac::Trellis().decode34(frame, block, true));
} else {
::memcpy(block, frame, 12U);
::memcpy(block + 12U, frame + 21U, 12U);
}
}
/**
* @brief Injects a data block into the given frame based on the data type.
* @param type The data type of the block to inject.
* @param block The input buffer containing the data block to inject.
* @param frame The output frame where the data block will be injected.
*/
void injectBlock(DataType::E type, const uint8_t* block, uint8_t* frame)
{
::memset(frame, 0x00U, DMR_FRAME_LENGTH_BYTES);
if (type == DataType::RATE_12_DATA) {
edac::BPTC19696().encode(block, frame);
} else if (type == DataType::RATE_34_DATA) {
edac::Trellis().encode34(block, frame, true);
} else {
::memcpy(frame, block, 12U);
::memcpy(frame + 21U, block + 12U, 12U);
}
}
}
TEST_CASE("DMR confirmed data blocks preserve DBSN, payload, and masked CRC-9", "[dmr][datablock]")
{
const DataType::E rates[] = {
DataType::RATE_12_DATA, DataType::RATE_34_DATA, DataType::RATE_1_DATA
};
for (DataType::E rate : rates) {
CAPTURE(rate);
const uint32_t length = payloadLength(rate, true);
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> payload{};
for (uint32_t i = 0U; i < length; ++i)
payload[i] = uint8_t(0x31U + i);
DataBlock encoded;
encoded.setFormat(DPF::CONFIRMED_DATA);
encoded.setDataType(rate);
encoded.setSerialNo(0x35U);
encoded.setData(payload.data());
std::array<uint8_t, DMR_FRAME_LENGTH_BYTES> frame{};
encoded.encode(frame.data());
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> raw{};
extractBlock(rate, frame.data(), raw.data());
REQUIRE((raw[0U] >> 1) == 0x35U);
REQUIRE(::memcmp(raw.data() + 2U, payload.data(), length) == 0);
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> crcInput{};
::memcpy(crcInput.data(), payload.data(), length);
for (uint32_t i = 0U; i < 7U; ++i)
WRITE_BIT(crcInput.data(), length * 8U + i, READ_BIT(raw.data(), i));
uint16_t expected = ~edac::CRC::createCRC9(crcInput.data(), length * 8U + 7U) & 0x1FFU;
expected ^= crcMask(rate);
const uint16_t transmitted = ((raw[0U] & 0x01U) << 8) | raw[1U];
REQUIRE(transmitted == expected);
DataHeader header;
header.setDPF(DPF::CONFIRMED_DATA);
DataBlock decoded;
decoded.setDataType(rate);
REQUIRE(decoded.decode(frame.data(), header));
REQUIRE(decoded.getSerialNo() == 0x35U);
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> output{};
REQUIRE(decoded.getData(output.data()) == length);
REQUIRE(::memcmp(output.data(), payload.data(), length) == 0);
}
}
TEST_CASE("DMR confirmed data blocks reject CRC-9 failures", "[dmr][datablock]")
{
const DataType::E rates[] = {
DataType::RATE_12_DATA, DataType::RATE_34_DATA, DataType::RATE_1_DATA
};
DataHeader header;
header.setDPF(DPF::CONFIRMED_DATA);
for (DataType::E rate : rates) {
CAPTURE(rate);
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> payload{};
payload.fill(0xA5U);
DataBlock encoded;
encoded.setFormat(header);
encoded.setDataType(rate);
encoded.setSerialNo(7U);
encoded.setData(payload.data());
std::array<uint8_t, DMR_FRAME_LENGTH_BYTES> frame{};
encoded.encode(frame.data());
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> raw{};
extractBlock(rate, frame.data(), raw.data());
raw[2U] ^= 0x80U;
injectBlock(rate, raw.data(), frame.data());
DataBlock decoded;
decoded.setDataType(rate);
REQUIRE_FALSE(decoded.decode(frame.data(), header));
}
}
TEST_CASE("DMR unconfirmed Rate 1 maps all 24 payload octets around the burst centre", "[dmr][datablock]")
{
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> payload{};
for (uint32_t i = 0U; i < payload.size(); ++i)
payload[i] = uint8_t(i + 1U);
DataBlock encoded;
encoded.setFormat(DPF::UNCONFIRMED_DATA);
encoded.setDataType(DataType::RATE_1_DATA);
encoded.setData(payload.data());
std::array<uint8_t, DMR_FRAME_LENGTH_BYTES> frame{};
frame.fill(0xFFU);
encoded.encode(frame.data());
REQUIRE((frame[12U] & 0xC0U) == 0U);
REQUIRE((frame[20U] & 0x03U) == 0U);
DataHeader header;
header.setDPF(DPF::UNCONFIRMED_DATA);
DataBlock decoded;
decoded.setDataType(DataType::RATE_1_DATA);
REQUIRE(decoded.decode(frame.data(), header));
std::array<uint8_t, DMR_PDU_UNCODED_LENGTH_BYTES> output{};
REQUIRE(decoded.getData(output.data()) == payload.size());
REQUIRE(output == payload);
}

@ -197,3 +197,23 @@ TEST_CASE("DataHeader copy and assignment preserve fields", "[dmr][dataheader]")
REQUIRE(assigned.getSrcId() == original.getSrcId());
REQUIRE(assigned.getDstId() == original.getDstId());
}
TEST_CASE("DataHeader preserves all six defined-short data format bits", "[dmr][dataheader]") {
const uint8_t formats[] = {0x01U, 0x15U, 0x2AU, 0x3FU};
for (uint8_t format : formats) {
uint8_t frame[DMR_FRAME_LENGTH_BYTES] = {};
DataHeader header;
header.setDPF(DPF::DEFINED_SHORT);
header.setSAP(0x0AU);
header.setSrcId(123U);
header.setDstId(456U);
header.setBlocksToFollow(1U);
header.setDataFormat(format);
header.encode(frame);
DataHeader decoded;
REQUIRE(decoded.decode(frame));
REQUIRE(decoded.getDataFormat() == format);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.