#2516 DMR Decoder enhancements. Architecture now shares common C4FM framework like P25P1 and NXDN. Adds soft decoding and enhanced/corrected error detection and correction. (#2517)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-26 06:34:57 -04:00
committed by GitHub
parent 5635341d12
commit f2c98cdb40
32 changed files with 4044 additions and 1282 deletions

View File

@@ -60,11 +60,11 @@ public class DmrSoftSyncDetectorTest
for(Dibit dibit: pattern.toDibits())
{
symbol = toSymbol(dibit);
scalar.processAndCalculate(symbol);
vector64.processAndCalculate(symbol);
vector128.processAndCalculate(symbol);
vector256.processAndCalculate(symbol);
vector512.processAndCalculate(symbol);
scalar.process(symbol);
vector64.process(symbol);
vector128.process(symbol);
vector256.process(symbol);
vector512.process(symbol);
}
assertSame(scalar.getDetectedPattern(), pattern, "Scalar Detector Did Not Correctly Detect Pattern: " + pattern);

View File

@@ -0,0 +1,399 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Golay24;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Utility for creating DMR data bursts and framing them for unit tests.
*/
public class DMRBurstTestUtility
{
public static final int BURST_DIBITS = 144;
public static final int DELAY_LINE_DIBITS = 90; //CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
public static final DMRSyncPattern SYNC_PATTERN = DMRSyncPattern.BASE_STATION_DATA;
public static final int CSBK_CRC_MASK = 0xA5A5;
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(15,
word -> Hamming15.getSyndrome(toMessage(word, 15), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(13,
word -> Hamming13.getSyndrome(toMessage(word, 13), new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) == 0);
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Finds the systematic codeword whose information bits (the low bits) match the information value.
*/
private static int encode(SoftBlockCode code, int information, int informationLength)
{
int mask = (1 << informationLength) - 1;
for(int codeword: code.getCodewords())
{
if((codeword & mask) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a random valid BPTC(196,96) codeword in transmitted (interleaved) bit order.
*/
public static CorrectedBinaryMessage createInterleavedBPTC(Random random)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196);
for(int row = 0; row < 9; row++)
{
int information = random.nextInt(1 << 11);
for(int column = 0; column < 11; column++)
{
if((information & (1 << column)) != 0)
{
deinterleaved.set(BPTC_196_96.getIndex(column, row));
}
}
}
return encodeAndInterleave(deinterleaved);
}
/**
* Creates a valid BPTC(196,96) codeword in transmitted (interleaved) bit order that carries the 96-bit payload,
* with the reserved bits set to zero.
* @param payload of 96 bits
*/
public static CorrectedBinaryMessage createInterleavedBPTC(BinaryMessage payload)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196);
//Payload bits occupy the row information columns in order, starting at index 4 (see BPTC_196_96.extract())
int payloadIndex = 0;
for(int index = 4; index < BPTC_196_96.MAX_ORIGINAL_INDEX && payloadIndex < 96; index++)
{
if((index % BPTC_196_96.COLUMN_COUNT) >= 1 && (index % BPTC_196_96.COLUMN_COUNT) <= 11)
{
if(payload.get(payloadIndex))
{
deinterleaved.set(index);
}
payloadIndex++;
}
}
return encodeAndInterleave(deinterleaved);
}
/**
* Encodes the row and column parity for the information bits (rows 0-8, columns 0-10) of the de-interleaved
* message and interleaves the result.
*/
private static CorrectedBinaryMessage encodeAndInterleave(CorrectedBinaryMessage deinterleaved)
{
//Information rows 0-8, each encoded with the row code
for(int row = 0; row < 9; row++)
{
int information = 0;
for(int column = 0; column < 11; column++)
{
if(deinterleaved.get(BPTC_196_96.getIndex(column, row)))
{
information |= (1 << column);
}
}
int rowWord = encode(ROW_CODE, information, 11);
for(int column = 11; column < 15; column++)
{
if((rowWord & (1 << column)) != 0)
{
deinterleaved.set(BPTC_196_96.getIndex(column, row));
}
}
}
//Parity rows 9-12 from the column code
for(int column = 0; column < 15; column++)
{
int information = 0;
for(int row = 0; row < 9; row++)
{
if(deinterleaved.get(BPTC_196_96.COLUMN_INDEXES[column][row]))
{
information |= (1 << row);
}
}
int columnWord = encode(COLUMN_CODE, information, 9);
for(int row = 9; row < 13; row++)
{
if((columnWord & (1 << row)) != 0)
{
deinterleaved.set(BPTC_196_96.COLUMN_INDEXES[column][row]);
}
}
}
//De-interleave maps deinterleaved[x] = interleaved[DEINTERLEAVE[x]], so interleave is the inverse
CorrectedBinaryMessage interleaved = new CorrectedBinaryMessage(196);
for(int x = 0; x < 196; x++)
{
if(deinterleaved.get(x))
{
interleaved.set(BPTC_196_96.BPTC_DEINTERLEAVE[x]);
}
}
return interleaved;
}
/**
* Creates a random 96-bit CSBK payload with a valid CRC-CCITT (80 message bits and 16 CRC bits).
*/
public static BinaryMessage createCSBKPayload(Random random)
{
BinaryMessage payload = new BinaryMessage(96);
int crc = CSBK_CRC_MASK;
for(int x = 0; x < 80; x++)
{
if(random.nextBoolean())
{
payload.set(x);
crc ^= CRCDMR.CCITT_80_CHECKSUMS[x];
}
}
for(int x = 0; x < 16; x++)
{
if((crc & (1 << (15 - x))) != 0)
{
payload.set(80 + x);
}
}
return payload;
}
/**
* Creates the 20 transmitted slot type bits: Golay(24,12) shortened by 4 leading zero bits, where the 24-bit
* message has the color code at bits 4-7, the data type at 8-11, the 11-bit checksum at 12-22 and even parity at 23.
*/
public static boolean[] createSlotType(int colorCode, DataType dataType)
{
boolean[] golay = new boolean[24];
int information = (colorCode << 4) | dataType.getValue();
int checksum = 0;
for(int bit = 0; bit < 8; bit++)
{
if((information & (1 << (7 - bit))) != 0)
{
golay[4 + bit] = true;
checksum ^= Golay24.CHECKSUMS[4 + bit];
}
}
int ones = Integer.bitCount(information);
for(int bit = 0; bit < 11; bit++)
{
if((checksum & (1 << (10 - bit))) != 0)
{
golay[12 + bit] = true;
ones++;
}
}
golay[23] = ones % 2 != 0;
boolean[] transmitted = new boolean[20];
System.arraycopy(golay, 4, transmitted, 0, 20);
return transmitted;
}
/**
* Creates a 288-bit DMR base station data burst: CACH (0-23), BPTC payload part 1 (24-121), slot type part 1
* (122-131), sync (132-179), slot type part 2 (180-189) and BPTC payload part 2 (190-287).
*/
public static boolean[] createBurst(CorrectedBinaryMessage bptc, int colorCode, DataType dataType)
{
boolean[] burst = new boolean[288];
boolean[] slotType = createSlotType(colorCode, dataType);
for(int x = 0; x < 98; x++)
{
burst[24 + x] = bptc.get(x);
burst[190 + x] = bptc.get(98 + x);
}
for(int x = 0; x < 10; x++)
{
burst[122 + x] = slotType[x];
burst[180 + x] = slotType[10 + x];
}
Dibit[] sync = SYNC_PATTERN.toDibits();
for(int x = 0; x < sync.length; x++)
{
burst[132 + 2 * x] = sync[x].getBit1();
burst[133 + 2 * x] = sync[x].getBit2();
}
return burst;
}
/**
* Converts the burst bits into ideal soft symbols.
*/
public static float[] toSoftSymbols(boolean[] burst)
{
float[] softSymbols = new float[BURST_DIBITS];
for(int x = 0; x < BURST_DIBITS; x++)
{
int value = (burst[2 * x] ? 2 : 0) + (burst[2 * x + 1] ? 1 : 0);
softSymbols[x] = Dibit.fromValue(value).getIdealPhase();
}
return softSymbols;
}
/**
* BPTC payload dibit indexes in the burst, excluding the CACH, slot type and sync dibits.
*/
public static List<Integer> getPayloadDibits()
{
List<Integer> payloadDibits = new ArrayList<>();
for(int x = 12; x < 61; x++)
{
payloadDibits.add(x);
}
for(int x = 95; x < BURST_DIBITS; x++)
{
payloadDibits.add(x);
}
return payloadDibits;
}
/**
* Moves the soft symbol just across the outer/inner (PI/2) decision boundary, flipping the low reliability
* magnitude bit of the symbol decision.
* @param softSymbols to modify
* @param burst bits to modify to match the new symbol decision
* @param dibit index of the symbol to modify
* @param margin distance from the decision boundary
*/
public static void addLowReliabilityError(float[] softSymbols, boolean[] burst, int dibit, float margin)
{
float sign = Math.signum(softSymbols[dibit]);
boolean outer = Math.abs(softSymbols[dibit]) > Math.PI / 2;
softSymbols[dibit] = sign * ((float)(Math.PI / 2) + (outer ? -margin : margin));
burst[2 * dibit + 1] = !burst[2 * dibit + 1];
}
/**
* Extracts the transmitted BPTC(196,96) bits from the burst.
*/
public static CorrectedBinaryMessage getBPTC(boolean[] burst)
{
CorrectedBinaryMessage bptc = new CorrectedBinaryMessage(196);
for(int x = 0; x < 98; x++)
{
bptc.set(x, burst[24 + x]);
bptc.set(98 + x, burst[190 + x]);
}
return bptc;
}
/**
* Feeds the burst soft symbols to the framer via the C4FM input, signals the sync detection on the final sync
* dibit, and then flushes the burst through the delay line with filler symbols.
* @return messages dispatched by the framer.
*/
public static List<IMessage> frame(float[] softSymbols)
{
List<IMessage> messages = new ArrayList<>();
DMRMessageFramer framer = new DMRMessageFramer(new DMRCrcMaskManager(false));
framer.setListener(messages::add);
framer.start();
//The sync pattern ends at dibit 89 (CACH 12 + prefix 54 + sync 24)
for(int x = 0; x < DELAY_LINE_DIBITS; x++)
{
framer.process(Dibit.fromSample(softSymbols[x]), softSymbols[x]);
}
framer.setSyncPattern(SYNC_PATTERN);
framer.syncDetected();
//Remaining burst dibits, followed by filler dibits to eject the full burst from the delay line
for(int x = DELAY_LINE_DIBITS; x < BURST_DIBITS + DELAY_LINE_DIBITS; x++)
{
float softSymbol = x < BURST_DIBITS ? softSymbols[x] : Dibit.D00_PLUS_1.getIdealPhase();
framer.process(Dibit.fromSample(softSymbol), softSymbol);
}
return messages;
}
}

View File

@@ -0,0 +1,160 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.DMRMessageFactory;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.SYNC_PATTERN;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.addLowReliabilityError;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createBurst;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createCSBKPayload;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createInterleavedBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.frame;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getPayloadDibits;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.toSoftSymbols;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR data message factory.
*/
public class DMRDataMessageFactoryTest
{
private static final int COLOR_CODE = 3;
private static final int ERROR_COUNT = 12;
//Soft symbol distance from the +/-3 and +/-1 decision boundary (PI/2) for low reliability symbol errors.
private static final float LOW_RELIABILITY_MARGIN = 0.05f;
/**
* Finds the CSBK message in the list of messages
*/
private static CSBKMessage getCSBK(List<IMessage> messages)
{
CSBKMessage csbk = null;
for(IMessage message: messages)
{
if(message instanceof CSBKMessage csbkMessage)
{
csbk = csbkMessage;
}
}
assertNotNull(csbk, "Expected a CSBK message: " + messages);
return csbk;
}
/**
* Test: an error free CSBK burst produces a valid CSBK message with the expected payload.
*/
@Test
void testValidCSBK()
{
Random random = new Random(1);
CorrectedBinaryMessage bptc = createInterleavedBPTC(createCSBKPayload(random));
CSBKMessage csbk = getCSBK(frame(toSoftSymbols(createBurst(bptc, COLOR_CODE, DataType.CSBK))));
assertTrue(csbk.isValid());
assertEquals(BPTC_196_96.extract(bptc).toString(), csbk.getMessage().toString());
}
/**
* Test: when the hard decision decoder converges on an incorrect codeword (miscorrection) and the CSBK fails the
* CRC check, the factory retries with soft decision decoding, which recovers the transmitted CSBK.
*
* Note: some miscorrections differ from the transmitted codeword by a single CRC protected bit, which the
* CSBK CRC check corrects. Those trials are verified to have the correct CRC protected content.
*/
@Test
void testSoftRetryAfterHardMiscorrection()
{
List<Integer> payloadDibits = getPayloadDibits();
DMRMessageFactory factory = new DMRMessageFactory(new DMRCrcMaskManager(false));
int crcFailures = 0;
for(int trial = 0; trial < 3000 && crcFailures < 5; trial++)
{
Random random = new Random(1000 + trial);
CorrectedBinaryMessage bptc = createInterleavedBPTC(createCSBKPayload(random));
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
boolean[] burst = createBurst(bptc, COLOR_CODE, DataType.CSBK);
float[] softSymbols = toSoftSymbols(burst);
Set<Integer> errors = new HashSet<>();
while(errors.size() < ERROR_COUNT)
{
errors.add(payloadDibits.get(random.nextInt(payloadDibits.size())));
}
for(int error: errors)
{
addLowReliabilityError(softSymbols, burst, error, LOW_RELIABILITY_MARGIN);
}
//Only evaluate trials where the hard decision decoder reports success with an incorrect codeword
CorrectedBinaryMessage hard = BPTC_196_96.extract(getBPTC(burst));
if(hard.getCorrectedBitCount() < 0 || hard.toString().equals(expected.toString()))
{
continue;
}
//Without soft bits, the factory creates the CSBK from the miscorrected payload
CorrectedBinaryMessage burstMessage = new CorrectedBinaryMessage(288);
for(int x = 0; x < 288; x++)
{
burstMessage.set(x, burst[x]);
}
IMessage hardMessage = factory.create(SYNC_PATTERN, burstMessage, null, CACH.getCACH(burstMessage), 0L, 1);
assertTrue(hardMessage instanceof CSBKMessage, "Trial " + trial);
if(hardMessage.isValid())
{
//The CRC check corrected the miscorrected payload bit, so the CRC protected content must be correct
assertEquals(expected.getSubMessage(0, 96).toString(),
((CSBKMessage)hardMessage).getMessage().getSubMessage(0, 96).toString(), "Trial " + trial);
continue;
}
crcFailures++;
//With soft symbols, the factory retries with soft decision decoding and recovers the transmitted CSBK
CSBKMessage csbk = getCSBK(frame(softSymbols));
assertTrue(csbk.isValid(), "Trial " + trial);
assertEquals(expected.toString(), csbk.getMessage().toString(), "Trial " + trial);
}
assertEquals(5, crcFailures, "Expected to find 5 hard decision miscorrections that fail the CRC check");
}
}

View File

@@ -0,0 +1,144 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.addLowReliabilityError;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createBurst;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createInterleavedBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.frame;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getPayloadDibits;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.toSoftSymbols;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR message framer C4FM symbol stream input, where the framer maintains the burst delay lines
* for the dibits and soft symbols and the sync detection is signalled on the final dibit of the sync pattern.
*/
public class DMRMessageFramerTest
{
private static final int COLOR_CODE = 5;
//Soft symbol distance from the +/-3 and +/-1 decision boundary (PI/2) for low reliability symbol errors.
private static final float LOW_RELIABILITY_MARGIN = 0.05f;
/**
* Finds the IDLE message dispatched by the framer
*/
private static IDLEMessage getIdleMessage(List<IMessage> messages)
{
IDLEMessage idle = null;
for(IMessage message: messages)
{
if(message instanceof IDLEMessage idleMessage)
{
idle = idleMessage;
}
}
assertNotNull(idle, "Framer did not dispatch an IDLE message: " + messages);
return idle;
}
/**
* Test: a burst fed through the C4FM input with the sync detection signalled on the final sync dibit is framed
* with the correct alignment so that the slot type and BPTC payload decode without errors.
*/
@Test
void testBurstAlignment()
{
Random random = new Random(1);
CorrectedBinaryMessage bptc = createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
assertEquals(0, expected.getCorrectedBitCount());
IDLEMessage idle = getIdleMessage(frame(toSoftSymbols(createBurst(bptc, COLOR_CODE, DataType.SLOT_IDLE))));
assertTrue(idle.isValid());
assertEquals(COLOR_CODE, idle.getSlotType().getColorCode());
assertEquals(DataType.SLOT_IDLE, idle.getSlotType().getDataType());
assertEquals(expected.toString(), idle.getMessage().toString());
assertEquals(0, idle.getMessage().getCorrectedBitCount());
}
/**
* Test: low reliability symbol errors in the BPTC payload that exceed the hard decision decoder capability are
* corrected by soft decision decoding, which requires the soft symbols to remain aligned with the dibits through
* the framer delay lines.
*
* Note: for IDLE messages (no CRC), soft decision decoding is only applied when the hard decision decoder fails,
* so trials where the hard decision decoder reports success (correctly, or with a miscorrection) don't exercise
* the soft symbols and are not evaluated.
*/
@Test
void testSoftSymbolAlignment()
{
List<Integer> payloadDibits = getPayloadDibits();
int hardFailures = 0;
for(int trial = 0; trial < 40; trial++)
{
Random random = new Random(100 + trial);
CorrectedBinaryMessage bptc = createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
boolean[] burst = createBurst(bptc, COLOR_CODE, DataType.SLOT_IDLE);
float[] softSymbols = toSoftSymbols(burst);
//Add 10 low reliability payload symbol errors
Set<Integer> errors = new HashSet<>();
while(errors.size() < 10)
{
errors.add(payloadDibits.get(random.nextInt(payloadDibits.size())));
}
for(int error: errors)
{
addLowReliabilityError(softSymbols, burst, error, LOW_RELIABILITY_MARGIN);
}
//Only evaluate trials where the hard decision decoder fails
if(BPTC_196_96.extract(getBPTC(burst)).getCorrectedBitCount() >= 0)
{
continue;
}
hardFailures++;
IDLEMessage idle = getIdleMessage(frame(softSymbols));
assertTrue(idle.isValid(), "Trial " + trial);
assertEquals(expected.toString(), idle.getMessage().toString(), "Trial " + trial);
}
//Sanity check that the soft decision decoder was exercised
assertTrue(hardFailures >= 5, "Expected at least 5 hard decision failures, but found " + hardFailures);
}
}

View File

@@ -0,0 +1,238 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming16;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.module.decode.dmr.message.voice.EMB;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for DMR soft decision decoding of the embedded signalling BPTC(128,77) product code and the EMB.
*/
public class BPTC_128_77SoftTest
{
private static final int[] WORD = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(16, word -> {
BinaryMessage message = new BinaryMessage(16);
for(int i = 0; i < 16; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return Hamming16.getSyndrome(message, 0) == 0;
});
/**
* Creates a random valid (de-interleaved) BPTC(128,77) product codeword: 7 random Hamming(16,11,4) rows followed
* by the column parity row.
*/
private static CorrectedBinaryMessage createCodeword(Random random)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(128);
int[] codewords = ROW_CODE.getCodewords();
int parityRow = 0;
for(int row = 0; row < 7; row++)
{
int rowWord = codewords[random.nextInt(codewords.length)];
parityRow ^= rowWord;
for(int column = 0; column < 16; column++)
{
if((rowWord & (1 << column)) != 0)
{
message.set(row * 16 + column);
}
}
}
for(int column = 0; column < 16; column++)
{
if((parityRow & (1 << column)) != 0)
{
message.set(7 * 16 + column);
}
}
return message;
}
/**
* Test: the row code is Hamming(16,11) with 2048 codewords and the generated product codewords are valid for the
* hard decision decoder.
*/
@Test
void testValidCodeword()
{
assertEquals(2048, ROW_CODE.getCodewords().length);
BPTC_128_77 bptc = new BPTC_128_77();
CorrectedBinaryMessage codeword = createCodeword(new Random(1));
assertTrue(bptc.getRowErrors(codeword).isEmpty());
assertTrue(bptc.getColumnErrors(codeword).isEmpty());
float[] softBits = new float[128];
for(int x = 0; x < 128; x++)
{
softBits[x] = codeword.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage decoded = bptc.decodeSoft(softBits);
assertNotNull(decoded);
assertEquals(codeword, decoded);
assertEquals(0, decoded.getCorrectedBitCount());
}
/**
* Test: soft decoding corrects 6 low reliability bit errors placed randomly across the product codeword.
*/
@Test
void testSoftDecodeRandomErrors()
{
Random random = new Random(2);
BPTC_128_77 bptc = new BPTC_128_77();
int hardFailures = 0;
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage codeword = createCodeword(random);
float[] softBits = new float[128];
for(int x = 0; x < 128; x++)
{
float magnitude = 0.6f + 0.8f * random.nextFloat();
softBits[x] = codeword.get(x) ? magnitude : -magnitude;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < 6)
{
errors.add(random.nextInt(128));
}
CorrectedBinaryMessage hard = new CorrectedBinaryMessage(codeword);
for(int error: errors)
{
softBits[error] = -0.3f * random.nextFloat() * Math.signum(softBits[error]);
hard.flip(error);
}
if(!bptc.correct(hard) || !hard.equals(codeword))
{
hardFailures++;
}
CorrectedBinaryMessage decoded = bptc.decodeSoft(softBits);
assertNotNull(decoded, "Soft decode failed on trial " + trial);
assertEquals(codeword, decoded, "Soft decode miscorrected on trial " + trial);
assertEquals(6, decoded.getCorrectedBitCount());
}
//Sanity check that this scenario exceeds the hard decision decoder capability
assertTrue(hardFailures > 0, "Expected hard decision failures");
}
/**
* Test: EMB soft decoding corrects 4 low reliability bit errors (beyond the hard decoder's 3-bit capability) and
* flags the result invalid since it exceeds the soft decision validity threshold. Also verifies that a single
* low reliability bit error is corrected and valid.
*/
@Test
void testEMBSoftDecode()
{
Random random = new Random(3);
CorrectedBinaryMessage reference = new CorrectedBinaryMessage(16);
for(int trial = 0; trial < 100; trial++)
{
//Find a valid word by hard decoding a random word with no more than 1 bit error after correction
int word;
EMB referenceEMB;
do
{
word = random.nextInt(1 << 16);
reference.setInt(word, WORD);
referenceEMB = new EMB(new CorrectedBinaryMessage(reference));
}
while(!referenceEMB.isValid());
//Recover the corrected valid word
CorrectedBinaryMessage corrected = new CorrectedBinaryMessage(reference);
new EMB(corrected);
int validWord = corrected.getInt(0, 15);
for(int errorCount: new int[]{1, 4})
{
float[] softBits = new float[16];
CorrectedBinaryMessage received = new CorrectedBinaryMessage(16);
received.setInt(validWord, WORD);
for(int x = 0; x < 16; x++)
{
softBits[x] = received.get(x) ? 1.0f : -1.0f;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < errorCount)
{
errors.add(random.nextInt(16));
}
for(int error: errors)
{
received.flip(error);
softBits[error] = -0.2f * softBits[error];
}
EMB emb = new EMB(received, softBits);
assertEquals(validWord, received.getInt(0, 15), "Trial " + trial + " errors " + errorCount);
if(errorCount == 1)
{
assertTrue(emb.isValid());
}
else
{
assertFalse(emb.isValid());
}
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for DMR soft decision decoding of the BPTC(196,96) product code.
*/
public class BPTC_196_96SoftTest
{
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(15,
word -> Hamming15.getSyndrome(toMessage(word, 15), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(13,
word -> Hamming13.getSyndrome(toMessage(word, 13), new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) == 0);
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Finds the systematic codeword whose information bits (the low bits) match the information value.
*/
private static int encode(SoftBlockCode code, int information, int informationLength)
{
int mask = (1 << informationLength) - 1;
for(int codeword: code.getCodewords())
{
if((codeword & mask) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a random valid (de-interleaved) BPTC(196,96) product codeword.
*/
private static CorrectedBinaryMessage createCodeword(Random random)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(196);
//Information rows 0-8, each encoded with the row code
for(int row = 0; row < 9; row++)
{
int rowWord = encode(ROW_CODE, random.nextInt(1 << 11), 11);
for(int x = 0; x < 15; x++)
{
if((rowWord & (1 << x)) != 0)
{
message.set(BPTC_196_96.getIndex(x, row));
}
}
}
//Parity rows 9-12 from the column code
for(int column = 0; column < 15; column++)
{
int information = 0;
for(int row = 0; row < 9; row++)
{
if(message.get(BPTC_196_96.COLUMN_INDEXES[column][row]))
{
information |= (1 << row);
}
}
int columnWord = encode(COLUMN_CODE, information, 9);
for(int row = 9; row < 13; row++)
{
if((columnWord & (1 << row)) != 0)
{
message.set(BPTC_196_96.COLUMN_INDEXES[column][row]);
}
}
}
return message;
}
/**
* Test: valid product codewords have no row or column errors and soft decoding of an error free codeword
* returns the codeword.
*/
@Test
void testValidCodeword()
{
Random random = new Random(2);
CorrectedBinaryMessage codeword = createCodeword(random);
CorrectedBinaryMessage copy = new CorrectedBinaryMessage(codeword);
assertTrue(BPTC_196_96.correct(copy, true));
assertEquals(codeword, copy);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
softBits[x] = codeword.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage decoded = BPTC_196_96.decodeSoft(softBits);
assertNotNull(decoded);
assertEquals(codeword, decoded);
assertEquals(0, decoded.getCorrectedBitCount());
}
/**
* Test: soft decoding corrects 10 low reliability bit errors placed randomly across the product codeword.
*/
@Test
void testSoftDecodeRandomErrors()
{
Random random = new Random(3);
int hardFailures = 0;
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage codeword = createCodeword(random);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
float magnitude = 0.6f + 0.8f * random.nextFloat();
softBits[x] = codeword.get(x) ? magnitude : -magnitude;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < 10)
{
errors.add(1 + random.nextInt(195));
}
CorrectedBinaryMessage hard = new CorrectedBinaryMessage(codeword);
for(int error: errors)
{
softBits[error] = -0.3f * random.nextFloat() * Math.signum(softBits[error]);
hard.flip(error);
}
if(!BPTC_196_96.correct(hard, true) || !hard.equals(codeword))
{
hardFailures++;
}
CorrectedBinaryMessage decoded = BPTC_196_96.decodeSoft(softBits);
assertNotNull(decoded, "Soft decode failed on trial " + trial);
assertEquals(codeword, decoded, "Soft decode miscorrected on trial " + trial);
assertEquals(10, decoded.getCorrectedBitCount());
}
//Sanity check that this scenario exceeds the hard decision decoder capability
assertTrue(hardFailures > 0, "Expected hard decision failures");
}
}

View File

@@ -0,0 +1,115 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR BPTC(196,96) hard decision decoder.
*/
public class BPTC_196_96Test
{
/**
* Test: when the hard decision decoder recovers the transmitted codeword, the corrected bit count equals the
* number of bit errors. The turbo decoder's incremental accounting previously counted some corrections twice
* (ie reporting 13-14 corrected bits for 10 bit errors).
*/
@Test
void testCorrectedBitCount()
{
Random random = new Random(1);
int[] correctDecodes = new int[9];
for(int errorCount = 1; errorCount <= 8; errorCount++)
{
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage bptc = DMRBurstTestUtility.createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
CorrectedBinaryMessage received = new CorrectedBinaryMessage(bptc);
//Bit 0 is a pad bit that isn't protected by the row or column codes
Set<Integer> errors = new HashSet<>();
while(errors.size() < errorCount)
{
errors.add(1 + random.nextInt(195));
}
for(int error: errors)
{
received.flip(error);
}
CorrectedBinaryMessage decoded = BPTC_196_96.extract(received);
if(decoded.getCorrectedBitCount() >= 0 && decoded.toString().equals(expected.toString()))
{
correctDecodes[errorCount]++;
assertEquals(errorCount, decoded.getCorrectedBitCount(), "Errors " + errorCount + " trial " + trial);
}
}
}
//Up to 2 bit errors are always corrected
assertEquals(200, correctDecodes[1]);
assertEquals(200, correctDecodes[2]);
for(int errorCount = 3; errorCount <= 8; errorCount++)
{
assertTrue(correctDecodes[errorCount] > 100, "Expected most decodes to be correct for " + errorCount +
" errors, but found " + correctDecodes[errorCount]);
}
}
/**
* Test: soft decision extraction of an error free codeword matches the hard decision extraction.
*/
@Test
void testExtractSoft()
{
Random random = new Random(2);
for(int trial = 0; trial < 20; trial++)
{
CorrectedBinaryMessage bptc = DMRBurstTestUtility.createInterleavedBPTC(random);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
softBits[x] = bptc.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage soft = BPTC_196_96.extractSoft(softBits);
assertNotNull(soft);
assertEquals(BPTC_196_96.extract(bptc).toString(), soft.toString());
assertEquals(0, soft.getCorrectedBitCount());
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* JUnit tests for the DMR Common Announcement Channel (CACH).
*/
public class CACHTest
{
/**
* Test: the CACH payload soft bits are de-interleaved consistently with the payload bits, so that each soft bit
* corresponds to the payload bit at the same position.
*/
@Test
void testPayloadSoftBits()
{
Random random = new Random(1);
for(int trial = 0; trial < 50; trial++)
{
CorrectedBinaryMessage burst = new CorrectedBinaryMessage(288);
float[] softBits = new float[288];
for(int x = 0; x < 288; x++)
{
if(random.nextBoolean())
{
burst.set(x);
}
//Unique magnitude per bit position so that a position mismatch can't go undetected
float magnitude = 1.0f + x / 1000.0f;
softBits[x] = burst.get(x) ? magnitude : -magnitude;
}
CACH cach = CACH.getCACH(burst, softBits);
BinaryMessage payload = cach.getPayload();
float[] payloadSoftBits = cach.getPayloadSoftBits();
assertNotNull(payloadSoftBits);
assertEquals(17, payloadSoftBits.length);
for(int x = 0; x < 17; x++)
{
assertEquals(payload.get(x), payloadSoftBits[x] > 0, "Trial " + trial + " payload bit " + x);
}
}
}
/**
* Test: payload soft bits are not available when the burst soft bits are not provided.
*/
@Test
void testNoSoftBits()
{
assertNull(CACH.getCACH(new CorrectedBinaryMessage(288)).getPayloadSoftBits());
assertNull(CACH.getCACH(new CorrectedBinaryMessage(288), null).getPayloadSoftBits());
}
}

View File

@@ -0,0 +1,328 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Hamming17;
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the short link control assembler, which reassembles 4 CACH fragments into a BPTC(68,36) protected
* short link control message with a CRC-8 check.
*/
public class SLCAssemblerTest
{
private static final int ROW_LENGTH = 17;
private static final int ROW_INFORMATION_LENGTH = 12;
private static final List<Integer> ROW_CODEWORDS = new ArrayList<>();
static
{
//Hamming(17,12) codewords where bit i of the codeword is message bit i
for(int word = 0; word < (1 << ROW_LENGTH); word++)
{
BinaryMessage message = new BinaryMessage(ROW_LENGTH);
for(int i = 0; i < ROW_LENGTH; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
if(Hamming17.getSyndrome(message, 0) == 0)
{
ROW_CODEWORDS.add(word);
}
}
}
/**
* Finds the systematic row codeword whose information bits (the low 12 bits) match the information value.
*/
private static int encodeRow(int information)
{
for(int codeword: ROW_CODEWORDS)
{
if((codeword & ((1 << ROW_INFORMATION_LENGTH) - 1)) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a 36-bit short link control payload of 28 random data bits followed by the CRC-8.
* @param random for the data bits
* @param validCRC true to append the correct CRC-8, or false to append a CRC-8 with one bit inverted.
*/
private static BinaryMessage createPayload(Random random, boolean validCRC)
{
BinaryMessage payload = new BinaryMessage(36);
for(int x = 0; x < 28; x++)
{
if(random.nextBoolean())
{
payload.set(x);
}
}
int crc = CRCDMR.crc8(payload, 28);
if(!validCRC)
{
crc ^= 1;
}
for(int x = 0; x < 8; x++)
{
if((crc & (1 << (7 - x))) != 0)
{
payload.set(28 + x);
}
}
return payload;
}
/**
* Encodes the 36-bit payload as a BPTC(68,36) message in transmitted (interleaved) bit order: 3 Hamming(17,12)
* rows carrying 12 payload bits each, followed by an even column parity row.
*/
private static BinaryMessage encode(BinaryMessage payload)
{
BinaryMessage deinterleaved = new BinaryMessage(68);
int parityRow = 0;
for(int row = 0; row < 4; row++)
{
int rowWord;
if(row < 3)
{
int information = 0;
for(int column = 0; column < ROW_INFORMATION_LENGTH; column++)
{
if(payload.get(row * ROW_INFORMATION_LENGTH + column))
{
information |= (1 << column);
}
}
rowWord = encodeRow(information);
parityRow ^= rowWord;
}
else
{
rowWord = parityRow;
}
for(int column = 0; column < ROW_LENGTH; column++)
{
if((rowWord & (1 << column)) != 0)
{
deinterleaved.set(row * ROW_LENGTH + column);
}
}
}
//De-interleave maps deinterleaved[(index * 17) % 67] = interleaved[index] (index 67 is not interleaved)
BinaryMessage interleaved = new BinaryMessage(68);
for(int index = 0; index < 67; index++)
{
if(deinterleaved.get((index * 17) % 67))
{
interleaved.set(index);
}
}
if(deinterleaved.get(67))
{
interleaved.set(67);
}
return interleaved;
}
/**
* Splits the interleaved message into 4 CACH fragments and processes them through the assembler.
* @return the short link control message dispatched with the last fragment.
*/
private static ShortLCMessage assemble(BinaryMessage interleaved)
{
return assemble(interleaved, null);
}
/**
* Splits the interleaved message and optional soft bits into 4 CACH fragments and processes them through the
* assembler.
* @param interleaved message
* @param softBits for the interleaved message, or null.
* @return the short link control message dispatched with the last fragment.
*/
private static ShortLCMessage assemble(BinaryMessage interleaved, float[] softBits)
{
SLCAssembler assembler = new SLCAssembler();
LCSS[] lcss = new LCSS[]{LCSS.FIRST_FRAGMENT, LCSS.CONTINUATION_FRAGMENT, LCSS.CONTINUATION_FRAGMENT,
LCSS.LAST_FRAGMENT};
ShortLCMessage message = null;
for(int fragment = 0; fragment < 4; fragment++)
{
BinaryMessage cachPayload = interleaved.getSubMessage(fragment * 17, fragment * 17 + 17);
float[] softFragment = softBits != null ? Arrays.copyOfRange(softBits, fragment * 17, fragment * 17 + 17) :
null;
message = assembler.process(lcss[fragment], cachPayload, softFragment, 0L);
if(fragment < 3)
{
assertNull(message, "Message dispatched before the last fragment");
}
}
assertNotNull(message, "Message not dispatched with the last fragment");
return message;
}
/**
* Test: an error free short link control message with a valid CRC is valid and carries the payload.
*/
@Test
void testValidMessage()
{
Random random = new Random(1);
for(int trial = 0; trial < 20; trial++)
{
BinaryMessage payload = createPayload(random, true);
ShortLCMessage message = assemble(encode(payload));
assertTrue(message.isValid(), "Trial " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(), "Trial " + trial);
}
}
/**
* Test: a short link control message with a single bit transmission error is corrected and valid.
*/
@Test
void testCorrectedMessage()
{
Random random = new Random(2);
for(int trial = 0; trial < 68; trial++)
{
BinaryMessage payload = createPayload(random, true);
BinaryMessage interleaved = encode(payload);
interleaved.flip(trial);
ShortLCMessage message = assemble(interleaved);
assertTrue(message.isValid(), "Bit error at " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(),
"Bit error at " + trial);
}
}
/**
* Test: a short link control message that passes BPTC error correction but fails the CRC-8 check is invalid.
* This occurs when the BPTC decoder converges on an incorrect codeword (miscorrection). The assembler previously
* overwrote the CRC-8 result with the BPTC result, reporting these messages as valid.
*/
@Test
void testInvalidCRC()
{
Random random = new Random(3);
for(int trial = 0; trial < 20; trial++)
{
ShortLCMessage message = assemble(encode(createPayload(random, false)));
assertEquals(0, message.getMessage().getCorrectedBitCount(), "Trial " + trial);
assertFalse(message.isValid(), "Trial " + trial);
}
}
/**
* Test: low reliability bit errors that defeat the hard decision decoder (decode failure, or a miscorrection that
* fails the CRC-8 check) are corrected by soft decision decoding when the fragment soft bits are provided.
*/
@Test
void testSoftDecision()
{
int hardFailures = 0;
for(int trial = 0; trial < 200 && hardFailures < 10; trial++)
{
Random random = new Random(100 + trial);
BinaryMessage payload = createPayload(random, true);
BinaryMessage interleaved = encode(payload);
float[] softBits = new float[68];
for(int x = 0; x < 68; x++)
{
softBits[x] = interleaved.get(x) ? 1.0f : -1.0f;
}
//Add 6 low reliability bit errors
Set<Integer> errors = new HashSet<>();
while(errors.size() < 6)
{
errors.add(random.nextInt(68));
}
for(int error: errors)
{
interleaved.flip(error);
softBits[error] = -0.2f * softBits[error];
}
//Only evaluate trials where the hard decision result is invalid
if(assemble(interleaved).isValid())
{
continue;
}
hardFailures++;
ShortLCMessage message = assemble(interleaved, softBits);
assertTrue(message.isValid(), "Trial " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(), "Trial " + trial);
}
assertEquals(10, hardFailures, "Expected to find 10 hard decision failures");
}
}