mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user