mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
* Adopts same architecture and symbol processor as P1 C4FM/LSM, DMR and NXDN. * Soft symbol sync detection, processing and erro correction. * Scalar and vector soft sync detection and calibration * Improved time slot detection/prediction and ISCH processing. * Fixes bug in parsing sequential mac structures in SACCH * Vector calibration(s) cleanup across multiple calibrations * Error correction bug in ISCH * Renames differential demod shared with P1 LSM decoder. Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.p25.phase1;
|
||||
package io.github.dsheirer.dsp.c4fm;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
@@ -28,9 +28,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* JUnit tests for the P25 Phase 1 LSM differential demodulator and differential integrator filter design.
|
||||
* JUnit tests for the DQPSK differential demodulator and differential integrator filter design.
|
||||
*/
|
||||
public class P25P1DifferentialDemodulatorLSMTest
|
||||
public class DQPSKDifferentialDemodulatorTest
|
||||
{
|
||||
private static final int SAMPLE_RATE = 24000;
|
||||
private static final int SAMPLES_PER_SYMBOL = 5;
|
||||
@@ -122,7 +122,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
void testOffsetRemovalAndSymbolRecovery()
|
||||
{
|
||||
SignalGenerator generator = new SignalGenerator(300, 0.0);
|
||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||
int symbolCount = 0;
|
||||
@@ -164,7 +164,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
void testAmbiguitySearch()
|
||||
{
|
||||
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||
boolean resolved = false;
|
||||
@@ -197,7 +197,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
void testSearchHoldsAcrossSignalGap()
|
||||
{
|
||||
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||
boolean resolved = false;
|
||||
@@ -250,8 +250,8 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
{
|
||||
SignalGenerator generatorUnverified = new SignalGenerator(-750, 0.1);
|
||||
SignalGenerator generatorVerified = new SignalGenerator(-750, 0.1);
|
||||
P25P1DifferentialDemodulatorLSM unverified = new P25P1DifferentialDemodulatorLSM();
|
||||
P25P1DifferentialDemodulatorLSM verified = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator unverified = new DQPSKDifferentialDemodulator();
|
||||
DQPSKDifferentialDemodulator verified = new DQPSKDifferentialDemodulator();
|
||||
unverified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
verified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||
@@ -287,7 +287,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
@Test
|
||||
void testReliabilities()
|
||||
{
|
||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
float[] amplitudes = new float[2 * BUFFER_LENGTH];
|
||||
|
||||
@@ -331,7 +331,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
||||
void testNoiseDoesNotMoveEstimate()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
||||
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||
|
||||
for(int buffer = 0; buffer < 2000; buffer++)
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.edac;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests the Reed-Solomon errors-and-erasures decoder against the P25 RS(63,35,29) code.
|
||||
*/
|
||||
public class ReedSolomonErasureDecoderTest
|
||||
{
|
||||
private static final int N = 63;
|
||||
private static final int K = 35;
|
||||
private static final int PARITY = N - K;
|
||||
private int[] randomCodeword(Random random)
|
||||
{
|
||||
int[] message = new int[K];
|
||||
|
||||
for(int i = 0; i < K; i++)
|
||||
{
|
||||
message[i] = random.nextInt(64);
|
||||
}
|
||||
|
||||
return ReedSolomonTestEncoder.encode(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrupts the codeword with the specified number of errors and erasures at distinct random positions.
|
||||
* @return erasure positions
|
||||
*/
|
||||
private int[] corrupt(int[] codeword, int errors, int erasures, Random random)
|
||||
{
|
||||
int[] positions = new int[N];
|
||||
|
||||
for(int i = 0; i < N; i++)
|
||||
{
|
||||
positions[i] = i;
|
||||
}
|
||||
|
||||
for(int i = N - 1; i > 0; i--)
|
||||
{
|
||||
int j = random.nextInt(i + 1);
|
||||
int t = positions[i];
|
||||
positions[i] = positions[j];
|
||||
positions[j] = t;
|
||||
}
|
||||
|
||||
for(int i = 0; i < errors; i++)
|
||||
{
|
||||
codeword[positions[i]] ^= 1 + random.nextInt(63);
|
||||
}
|
||||
|
||||
int[] erased = Arrays.copyOfRange(positions, errors, errors + erasures);
|
||||
|
||||
for(int position : erased)
|
||||
{
|
||||
codeword[position] = random.nextInt(64);
|
||||
}
|
||||
|
||||
return erased;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encoderProducesValidCodewords()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
|
||||
|
||||
for(int trial = 0; trial < 100; trial++)
|
||||
{
|
||||
int[] codeword = randomCodeword(random);
|
||||
int[] output = new int[N];
|
||||
assertFalse(reference.decode(codeword, output));
|
||||
assertArrayEquals(codeword, output);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctsErrorsAndErasuresWithinCapacity()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||
|
||||
for(int erasures = 0; erasures <= PARITY; erasures++)
|
||||
{
|
||||
int maxErrors = (PARITY - erasures) / 2;
|
||||
|
||||
for(int errors = 0; errors <= maxErrors; errors++)
|
||||
{
|
||||
for(int trial = 0; trial < 20; trial++)
|
||||
{
|
||||
int[] codeword = randomCodeword(random);
|
||||
int[] received = codeword.clone();
|
||||
int[] erased = corrupt(received, errors, erasures, random);
|
||||
int[] output = new int[N];
|
||||
assertFalse(decoder.decode(received, output, erased, erased.length),
|
||||
"Failed with errors [" + errors + "] erasures [" + erasures + "]");
|
||||
assertArrayEquals(codeword, output, "Errors [" + errors + "] erasures [" + erasures + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsMostDecodingFailuresBeyondCapacity()
|
||||
{
|
||||
Random random = new Random(3);
|
||||
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||
int miscorrections = 0;
|
||||
int trials = 1000;
|
||||
|
||||
for(int trial = 0; trial < trials; trial++)
|
||||
{
|
||||
int[] codeword = randomCodeword(random);
|
||||
int[] received = codeword.clone();
|
||||
int erasures = 6;
|
||||
int errors = (PARITY - erasures) / 2 + 3;
|
||||
int[] erased = corrupt(received, errors, erasures, random);
|
||||
int[] output = new int[N];
|
||||
|
||||
if(!decoder.decode(received, output, erased, erased.length))
|
||||
{
|
||||
//A miscorrection must still produce a valid (but incorrect) codeword
|
||||
assertFalse(Arrays.equals(codeword, output));
|
||||
int[] check = new int[N];
|
||||
assertFalse(new ReedSolomon_63_35_29_P25().decode(output, check));
|
||||
miscorrections++;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(miscorrections < trials / 100, "Miscorrections: " + miscorrections);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesErrorsOnlyDecoder()
|
||||
{
|
||||
Random random = new Random(4);
|
||||
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
|
||||
|
||||
for(int trial = 0; trial < 500; trial++)
|
||||
{
|
||||
int[] received = randomCodeword(random);
|
||||
corrupt(received, random.nextInt(18), 0, random);
|
||||
int[] output = new int[N];
|
||||
int[] referenceOutput = new int[N];
|
||||
boolean failed = decoder.decode(received, output, new int[0], 0);
|
||||
boolean referenceFailed = reference.decode(received, referenceOutput);
|
||||
assertEquals(referenceFailed, failed);
|
||||
|
||||
if(!failed)
|
||||
{
|
||||
assertArrayEquals(referenceOutput, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.edac;
|
||||
|
||||
/**
|
||||
* Systematic encoder for the P25 RS(63,35,29) code (and codes shortened from it) for unit tests. Parity symbols are
|
||||
* at indices 0-27 and message symbols at indices 28-62, with generator polynomial roots alpha^1 through alpha^28.
|
||||
*/
|
||||
public class ReedSolomonTestEncoder
|
||||
{
|
||||
private static final int N = 63;
|
||||
private static final int K = 35;
|
||||
private static final int PARITY = N - K;
|
||||
private static final int[] EXP = new int[63];
|
||||
private static final int[] LOG = new int[64];
|
||||
|
||||
static
|
||||
{
|
||||
int value = 1;
|
||||
|
||||
for(int i = 0; i < 63; i++)
|
||||
{
|
||||
EXP[i] = value;
|
||||
LOG[value] = i;
|
||||
value <<= 1;
|
||||
|
||||
if(value > 63)
|
||||
{
|
||||
value = (value & 63) ^ 3; //x^6 = x + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int multiply(int a, int b)
|
||||
{
|
||||
return (a == 0 || b == 0) ? 0 : EXP[(LOG[a] + LOG[b]) % 63];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the message symbols.
|
||||
* @param message symbols (35), where trailing zero symbols represent a shortened code
|
||||
* @return codeword (63 symbols)
|
||||
*/
|
||||
public static int[] encode(int[] message)
|
||||
{
|
||||
int[] generator = new int[PARITY + 1];
|
||||
generator[0] = 1;
|
||||
|
||||
for(int i = 1; i <= PARITY; i++)
|
||||
{
|
||||
int root = EXP[i % 63];
|
||||
|
||||
for(int j = i; j > 0; j--)
|
||||
{
|
||||
generator[j] = generator[j - 1] ^ multiply(generator[j], root);
|
||||
}
|
||||
|
||||
generator[0] = multiply(generator[0], root);
|
||||
}
|
||||
|
||||
//Remainder of x^28 * m(x) divided by g(x)
|
||||
int[] remainder = new int[N];
|
||||
System.arraycopy(message, 0, remainder, PARITY, K);
|
||||
|
||||
for(int i = N - 1; i >= PARITY; i--)
|
||||
{
|
||||
int coefficient = remainder[i];
|
||||
|
||||
if(coefficient != 0)
|
||||
{
|
||||
for(int j = 0; j <= PARITY; j++)
|
||||
{
|
||||
remainder[i - PARITY + j] ^= multiply(generator[j], coefficient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[] codeword = new int[N];
|
||||
System.arraycopy(remainder, 0, codeword, 0, PARITY);
|
||||
System.arraycopy(message, 0, codeword, PARITY, K);
|
||||
return codeword;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.edac.bch.BCH_63_16_23_P25;
|
||||
import io.github.dsheirer.edac.trellis.P25_1_2_Node;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SyncDetector;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
@@ -252,7 +253,7 @@ public class P25P1DecoderLSMTest
|
||||
private static List<Dibit> getFrame(BitSet tsbk)
|
||||
{
|
||||
List<Dibit> data = new ArrayList<>();
|
||||
addBits(data, 0x5575F5FF77FFL, 48);
|
||||
addBits(data, P25P1SyncDetector.SYNC_PATTERN, 48);
|
||||
addBits(data, BCH_63_16_23_P25.encode(NAC, TSBK_DUID) << 1, 64); //64th (parity) bit is zero
|
||||
data.addAll(encodeTSBK(tsbk));
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase1.sync;
|
||||
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* JUnit tests for the P25 Phase 1 soft sync detector scalar and SIMD vector implementations.
|
||||
*/
|
||||
public class P25P1SoftSyncDetectorTest
|
||||
{
|
||||
private static final float IDEAL_SCORE = (float)(24 * Math.pow(3.0 * Math.PI / 4.0, 2));
|
||||
|
||||
private static P25P1SoftSyncDetector[] getDetectors()
|
||||
{
|
||||
return new P25P1SoftSyncDetector[]{new P25P1SoftSyncDetectorScalar(), new P25P1SoftSyncDetectorVector64(),
|
||||
new P25P1SoftSyncDetectorVector128(), new P25P1SoftSyncDetectorVector256(),
|
||||
new P25P1SoftSyncDetectorVector512()};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsSyncPattern()
|
||||
{
|
||||
for(P25P1SoftSyncDetector detector : getDetectors())
|
||||
{
|
||||
float[] pattern = detector.getPatternSymbols();
|
||||
float score = 0;
|
||||
|
||||
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
|
||||
for(int repeat = 0; repeat < 3; repeat++)
|
||||
{
|
||||
for(float symbol : pattern)
|
||||
{
|
||||
score = detector.process(symbol);
|
||||
}
|
||||
|
||||
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vectorImplementationsMatchScalar()
|
||||
{
|
||||
P25P1SoftSyncDetector[] detectors = getDetectors();
|
||||
Random random = new Random(1);
|
||||
|
||||
for(int x = 0; x < 1000; x++)
|
||||
{
|
||||
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
|
||||
float expected = detectors[0].process(symbol);
|
||||
|
||||
for(int d = 1; d < detectors.length; d++)
|
||||
{
|
||||
assertEquals(expected, detectors[d].process(symbol), 0.001,
|
||||
detectors[d].getClass().getSimpleName() + " at symbol " + x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase2;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.bits.FragmentedIntField;
|
||||
import io.github.dsheirer.bits.IntField;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.edac.CRCP25;
|
||||
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetector;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.FacchTimeslot;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.ScramblingSequence;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
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.assertTrue;
|
||||
|
||||
/**
|
||||
* End-to-end JUnit tests for the P25 Phase 2 HDQPSK decoder: a synthetic outbound (FDMA) channel carrying super frame
|
||||
* fragments with I-ISCH, S-ISCH (sync) and four scrambled FACCH timeslots is generated and decoded, and each valid
|
||||
* decoded MAC message is checked against the transmitted MAC messages.
|
||||
*/
|
||||
public class P25P2DecoderHDQPSKTest
|
||||
{
|
||||
private static final double SAMPLE_RATE = 48000.0;
|
||||
private static final double SYMBOL_RATE = 6000.0;
|
||||
private static final int FRAGMENT_COUNT = 60;
|
||||
private static final int TRAILING_FRAGMENT_COUNT = 2;
|
||||
private static final ScrambleParameters SCRAMBLE_PARAMETERS = new ScrambleParameters(0xBEE00, 0x3BB, 0x3B2);
|
||||
private static final long ISCH_CODE_WORD_OFFSET = 0x184229d461L;
|
||||
private static final int[] DUID_BITS = {0, 1, 74, 75, 244, 245, 318, 319};
|
||||
//FACCH MAC PDU: 156 bits, with the CRC-12 checksum in the final 12 bits
|
||||
private static final int MAC_LENGTH = 156;
|
||||
private static final int MAC_CRC_START = 144;
|
||||
private static final int MAC_PDU_TYPE_IDLE = 3;
|
||||
//Raised cosine transmit pulse
|
||||
private static final double ROLLOFF = 0.2;
|
||||
private static final int PULSE_SPAN_SYMBOLS = 8;
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
private static final Dibit[] DIBITS = {Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3};
|
||||
private static final int MESSAGES_PER_FRAGMENT = 4;
|
||||
|
||||
/**
|
||||
* Test: carrier offsets beyond the fourth power offset estimator range are resolved by the offset ambiguity search,
|
||||
* after which every FACCH MAC message decodes. A fragment framed while the search steps to the correct offset range
|
||||
* can produce an invalid (CRC failure) MAC message.
|
||||
*/
|
||||
@Test
|
||||
void testOffsetAmbiguitySearch()
|
||||
{
|
||||
for(double carrierOffset : new double[]{1000.0, -1000.0, 1200.0, -1200.0})
|
||||
{
|
||||
Result result = decode(carrierOffset, -100.0, Double.NaN, 3, false);
|
||||
assertTrue(result.invalid() <= 1, "Invalid messages at " + carrierOffset + " Hz: " + result.invalid());
|
||||
assertTrue(result.first() <= 12, "Acquisition at " + carrierOffset + " Hz: " + result.first() + " fragments");
|
||||
assertEquals(0, result.missed(), "Missed messages after acquisition at " + carrierOffset + " Hz");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: with additive white gaussian noise at 12 dB in-band (+/- 3.6 kHz) carrier to noise, nearly all FACCH MAC
|
||||
* messages decode and every valid MAC message matches a transmitted MAC message.
|
||||
*/
|
||||
@Test
|
||||
void testNoise()
|
||||
{
|
||||
Result result = decode(200.0, 50.0, 12.0, 5, false);
|
||||
assertTrue(result.valid() >= FRAGMENT_COUNT * MESSAGES_PER_FRAGMENT * 0.9, "Valid messages: " + result.valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and decodes the synthetic signal.
|
||||
* @param carrierOffset in Hertz
|
||||
* @param symbolRateError in parts per million
|
||||
* @param carrierToNoise in dB, in-band (+/- 3.6 kHz), or NaN for no noise
|
||||
* @param seed for the random MAC payloads and noise
|
||||
* @param corruptIsch to corrupt both I-ISCH in the second and third fragment of each superframe
|
||||
* @return decoded message counts (valid messages for the checked fragments), the index of the first fragment with a decoded MAC message (the fragment count
|
||||
* when none decode), the count of MAC messages that didn't decode after that fragment, and the count of valid MAC
|
||||
* messages decoded from the replayed dibit stream.
|
||||
*/
|
||||
private static Result decode(double carrierOffset, double symbolRateError, double carrierToNoise, long seed,
|
||||
boolean corruptIsch)
|
||||
{
|
||||
Random random = new Random(seed);
|
||||
ScramblingSequence scramblingSequence = new ScramblingSequence();
|
||||
scramblingSequence.update(SCRAMBLE_PARAMETERS);
|
||||
List<BinaryMessage> macs = new ArrayList<>();
|
||||
Set<BinaryMessage> transmitted = new HashSet<>();
|
||||
List<Dibit> dibits = new ArrayList<>();
|
||||
|
||||
for(int fragment = 0; fragment < FRAGMENT_COUNT + TRAILING_FRAGMENT_COUNT; fragment++)
|
||||
{
|
||||
List<BinaryMessage> fragmentMacs = new ArrayList<>();
|
||||
|
||||
for(int x = 0; x < MESSAGES_PER_FRAGMENT; x++)
|
||||
{
|
||||
fragmentMacs.add(getMacMessage(random));
|
||||
}
|
||||
|
||||
//Trailing fragments aren't checked since the final fragment isn't dispatched until its trailing symbols are
|
||||
//processed, but they are included in the transmitted messages for the CRC false positive check.
|
||||
if(fragment < FRAGMENT_COUNT)
|
||||
{
|
||||
macs.addAll(fragmentMacs);
|
||||
}
|
||||
|
||||
transmitted.addAll(fragmentMacs);
|
||||
|
||||
boolean corrupt = corruptIsch && (fragment % 3) != 0;
|
||||
dibits.addAll(getFragment(fragment, fragmentMacs, scramblingSequence, corrupt, random));
|
||||
}
|
||||
|
||||
float[][] samples = modulate(dibits, carrierOffset, symbolRateError, carrierToNoise, random);
|
||||
|
||||
List<MacMessage> received = new ArrayList<>();
|
||||
List<ByteBuffer> buffers = new ArrayList<>();
|
||||
DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2();
|
||||
config.setScrambleParameters(SCRAMBLE_PARAMETERS);
|
||||
P25P2DecoderHDQPSK decoder = new P25P2DecoderHDQPSK(config);
|
||||
decoder.setMessageListener((IMessage message) -> {
|
||||
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH())
|
||||
{
|
||||
received.add(mac);
|
||||
}
|
||||
});
|
||||
decoder.setBufferListener(buffer -> {
|
||||
ByteBuffer copy = ByteBuffer.allocate(buffer.capacity());
|
||||
copy.put(buffer.array());
|
||||
buffers.add(copy);
|
||||
});
|
||||
decoder.start();
|
||||
decoder.setSampleRate(SAMPLE_RATE);
|
||||
|
||||
for(int offset = 0; offset < samples[0].length; offset += BUFFER_SIZE)
|
||||
{
|
||||
int length = Math.min(BUFFER_SIZE, samples[0].length - offset);
|
||||
float[] i = new float[length];
|
||||
float[] q = new float[length];
|
||||
System.arraycopy(samples[0], offset, i, 0, length);
|
||||
System.arraycopy(samples[1], offset, q, 0, length);
|
||||
decoder.receive(new ComplexSamples(i, q, 0L));
|
||||
}
|
||||
|
||||
decoder.stop();
|
||||
|
||||
int invalid = 0;
|
||||
Set<BinaryMessage> decoded = new HashSet<>();
|
||||
|
||||
for(MacMessage mac : received)
|
||||
{
|
||||
if(mac.isValid())
|
||||
{
|
||||
BinaryMessage bits = getBits(mac.getMessage());
|
||||
//A valid MAC message that doesn't match a transmitted MAC message is a CRC false positive
|
||||
assertTrue(transmitted.contains(bits), "Decoded MAC message was not transmitted");
|
||||
assertTrue(decoded.add(bits), "Decoded MAC message was duplicated");
|
||||
}
|
||||
else
|
||||
{
|
||||
invalid++;
|
||||
}
|
||||
}
|
||||
|
||||
int valid = 0;
|
||||
|
||||
int first = FRAGMENT_COUNT;
|
||||
int missed = 0;
|
||||
|
||||
for(int x = 0; x < macs.size(); x++)
|
||||
{
|
||||
int fragment = x / MESSAGES_PER_FRAGMENT;
|
||||
|
||||
if(decoded.contains(macs.get(x)))
|
||||
{
|
||||
valid++;
|
||||
first = Math.min(first, fragment);
|
||||
}
|
||||
else if(first < fragment)
|
||||
{
|
||||
missed++;
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(valid, invalid, first, missed, replay(buffers, new HashSet<>(macs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays the demodulated dibit stream through a message framer with hard sync detection.
|
||||
* @return count of valid MAC messages that match a checked (transmitted) MAC message.
|
||||
*/
|
||||
private static int replay(List<ByteBuffer> buffers, Set<BinaryMessage> transmitted)
|
||||
{
|
||||
Set<BinaryMessage> decoded = new HashSet<>();
|
||||
P25P2MessageFramer framer = new P25P2MessageFramer();
|
||||
framer.setScrambleParameters(SCRAMBLE_PARAMETERS);
|
||||
P25P2MessageProcessor processor = new P25P2MessageProcessor();
|
||||
processor.setMessageListener((IMessage message) -> {
|
||||
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH() && mac.isValid())
|
||||
{
|
||||
BinaryMessage bits = getBits(mac.getMessage());
|
||||
|
||||
if(transmitted.contains(bits))
|
||||
{
|
||||
decoded.add(bits);
|
||||
}
|
||||
}
|
||||
});
|
||||
framer.setListener(processor);
|
||||
framer.start();
|
||||
|
||||
for(ByteBuffer buffer : buffers)
|
||||
{
|
||||
framer.receive(buffer);
|
||||
}
|
||||
|
||||
return decoded.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the MAC message bits for comparison against the transmitted MAC messages.
|
||||
*/
|
||||
private static BinaryMessage getBits(CorrectedBinaryMessage message)
|
||||
{
|
||||
BinaryMessage bits = new BinaryMessage(MAC_LENGTH);
|
||||
|
||||
for(int x = 0; x < MAC_LENGTH; x++)
|
||||
{
|
||||
if(message.get(x))
|
||||
{
|
||||
bits.set(x);
|
||||
}
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random MAC IDLE PDU with a NULL INFORMATION MAC structure, random payload bits and the CRC-12 checksum.
|
||||
*/
|
||||
private static BinaryMessage getMacMessage(Random random)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(MAC_LENGTH);
|
||||
message.load(0, 3, MAC_PDU_TYPE_IDLE);
|
||||
//Opcode (bits 8-15) is zero for the NULL INFORMATION MAC structure
|
||||
|
||||
for(int x = 16; x < MAC_CRC_START; x++)
|
||||
{
|
||||
if(random.nextBoolean())
|
||||
{
|
||||
message.set(x);
|
||||
}
|
||||
}
|
||||
|
||||
//The checksum isn't included in the CRC calculation, so exactly one checksum value passes the CRC check
|
||||
for(int checksum = 0; checksum < 4096; checksum++)
|
||||
{
|
||||
message.load(MAC_CRC_START, 12, checksum);
|
||||
|
||||
if(CRCP25.crc12_FACCH(message))
|
||||
{
|
||||
return getBits(message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to calculate the CRC-12 checksum");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the 720 dibit super frame fragment: I-ISCH 1, timeslot A, I-ISCH 2, timeslot B, S-ISCH (sync),
|
||||
* timeslot C, S-ISCH (sync), timeslot D.
|
||||
* @param fragment index, which determines the ISCH sequence (fragment 1-3 of the superframe)
|
||||
* @param macs for timeslots A-D
|
||||
* @param scramblingSequence to scramble the timeslots
|
||||
* @param corruptIsch to corrupt both I-ISCH beyond the error correction capability
|
||||
* @param random for selecting the corrupted bits
|
||||
*/
|
||||
private static List<Dibit> getFragment(int fragment, List<BinaryMessage> macs, ScramblingSequence scramblingSequence,
|
||||
boolean corruptIsch, Random random)
|
||||
{
|
||||
int ischSequence = fragment % 3;
|
||||
int superframeSequence = (fragment / 3) % 4;
|
||||
List<Dibit> dibits = new ArrayList<>();
|
||||
|
||||
for(int position = 0; position < 4; position++)
|
||||
{
|
||||
switch(position)
|
||||
{
|
||||
case 0, 1 -> addBits(dibits, getIISCH(position, ischSequence, superframeSequence, corruptIsch, random), 40);
|
||||
default -> addBits(dibits, P25P2SoftSyncDetector.SYNC_PATTERN, 40);
|
||||
}
|
||||
|
||||
BinaryMessage sequence = scramblingSequence.getTimeslotSequence(ischSequence * 4 + position);
|
||||
BinaryMessage timeslot = getTimeslot(macs.get(position), sequence);
|
||||
|
||||
for(int x = 0; x < 320; x += 2)
|
||||
{
|
||||
dibits.add(DIBITS[(timeslot.get(x) ? 2 : 0) + (timeslot.get(x + 1) ? 1 : 0)]);
|
||||
}
|
||||
}
|
||||
|
||||
return dibits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the 40-bit I-ISCH codeword.
|
||||
* @param channel 0 (timeslot 1) or 1 (timeslot 2)
|
||||
* @param ischSequence 0-2 (fragment 1-3 of the superframe)
|
||||
* @param superframeSequence 0-3
|
||||
* @param corrupt to flip 12 codeword bits, which exceeds the 7 bit error correction capability
|
||||
* @param random for selecting the corrupted bits
|
||||
*/
|
||||
private static long getIISCH(int channel, int ischSequence, int superframeSequence, boolean corrupt, Random random)
|
||||
{
|
||||
BinaryMessage message = new BinaryMessage(9);
|
||||
message.load(2, 2, channel);
|
||||
message.load(4, 2, ischSequence);
|
||||
message.load(7, 2, superframeSequence);
|
||||
long codeword = ISCHDecoder.getCodeWord(message) ^ ISCH_CODE_WORD_OFFSET;
|
||||
|
||||
if(corrupt)
|
||||
{
|
||||
Set<Integer> bits = new HashSet<>();
|
||||
|
||||
while(bits.size() < 12)
|
||||
{
|
||||
bits.add(random.nextInt(40));
|
||||
}
|
||||
|
||||
for(int bit : bits)
|
||||
{
|
||||
codeword ^= 1L << bit;
|
||||
}
|
||||
}
|
||||
|
||||
return codeword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the scrambled 320-bit FACCH timeslot: RS(63,35,29) encodes the 26 MAC message hexbits, loads the message
|
||||
* and transmitted (19) parity hexbits into the FACCH timeslot fields, scrambles the timeslot, and sets the
|
||||
* (unscrambled) data unit ID.
|
||||
*/
|
||||
private static BinaryMessage getTimeslot(BinaryMessage mac, BinaryMessage scramblingSequence)
|
||||
{
|
||||
//Message hexbit h (INFO_h+1) is codeword index 53 - h, and parity hexbit p (PARITY_p) is codeword index 28 - p
|
||||
int[] message = new int[35];
|
||||
|
||||
for(int h = 0; h < 26; h++)
|
||||
{
|
||||
message[25 - h] = mac.getInt(h * 6, h * 6 + 5);
|
||||
}
|
||||
|
||||
int[] codeword = ReedSolomonTestEncoder.encode(message);
|
||||
BinaryMessage timeslot = new BinaryMessage(320);
|
||||
|
||||
for(int h = 1; h <= 26; h++)
|
||||
{
|
||||
load(timeslot, getFacchField("INFO_" + h), codeword[54 - h]);
|
||||
}
|
||||
|
||||
for(int p = 1; p <= 19; p++)
|
||||
{
|
||||
load(timeslot, getFacchField("PARITY_" + p), codeword[28 - p]);
|
||||
}
|
||||
|
||||
timeslot.xor(scramblingSequence);
|
||||
|
||||
int duid = DataUnitID.SCRAMBLED_FACCH.getValueWithParity();
|
||||
|
||||
for(int x = 0; x < DUID_BITS.length; x++)
|
||||
{
|
||||
if(((duid >> (7 - x)) & 1) == 1)
|
||||
{
|
||||
timeslot.set(DUID_BITS[x]);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeslot.clear(DUID_BITS[x]);
|
||||
}
|
||||
}
|
||||
|
||||
return timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses the FACCH timeslot hexbit field (IntField or FragmentedIntField) by name.
|
||||
*/
|
||||
private static Object getFacchField(String name)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field field = FacchTimeslot.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field.get(null);
|
||||
}
|
||||
catch(ReflectiveOperationException e)
|
||||
{
|
||||
throw new IllegalStateException("FACCH timeslot field " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the 6-bit value into the field bits, most significant bit first.
|
||||
*/
|
||||
private static void load(BinaryMessage message, Object field, int value)
|
||||
{
|
||||
int[] indices;
|
||||
|
||||
if(field instanceof IntField intField)
|
||||
{
|
||||
indices = new int[intField.width()];
|
||||
|
||||
for(int x = 0; x < indices.length; x++)
|
||||
{
|
||||
indices[x] = intField.start() + x;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
indices = ((FragmentedIntField)field).indices();
|
||||
}
|
||||
|
||||
for(int x = 0; x < indices.length; x++)
|
||||
{
|
||||
if(((value >> (indices.length - 1 - x)) & 1) == 1)
|
||||
{
|
||||
message.set(indices[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulates the dibits as HDQPSK: differential QPSK with raised cosine pulse shaping.
|
||||
* @return i and q sample arrays
|
||||
*/
|
||||
private static float[][] modulate(List<Dibit> dibits, double carrierOffset, double symbolRateError,
|
||||
double carrierToNoise, Random random)
|
||||
{
|
||||
double[] symbolI = new double[dibits.size()];
|
||||
double[] symbolQ = new double[dibits.size()];
|
||||
double phase = 0.0;
|
||||
|
||||
for(int x = 0; x < dibits.size(); x++)
|
||||
{
|
||||
phase += dibits.get(x).getIdealPhase();
|
||||
symbolI[x] = Math.cos(phase);
|
||||
symbolQ[x] = Math.sin(phase);
|
||||
}
|
||||
|
||||
double samplesPerSymbol = SAMPLE_RATE / (SYMBOL_RATE * (1.0 + symbolRateError * 1e-6));
|
||||
//The decimation filter requires an even sample count
|
||||
int length = (int)((dibits.size() + PULSE_SPAN_SYMBOLS) * samplesPerSymbol) / 2 * 2;
|
||||
float[] i = new float[length];
|
||||
float[] q = new float[length];
|
||||
|
||||
//Unit power symbols: the in-band noise variance is the in-band fraction of the total noise variance
|
||||
double noise = Double.isNaN(carrierToNoise) ? 0.0 :
|
||||
Math.sqrt(Math.pow(10.0, -carrierToNoise / 10.0) * SAMPLE_RATE / 7200.0 / 2.0);
|
||||
|
||||
for(int n = 0; n < length; n++)
|
||||
{
|
||||
double t = n / samplesPerSymbol - PULSE_SPAN_SYMBOLS / 2.0; //Symbol periods
|
||||
double sumI = 0.0;
|
||||
double sumQ = 0.0;
|
||||
|
||||
for(int k = (int)Math.ceil(t - PULSE_SPAN_SYMBOLS / 2.0); k <= t + PULSE_SPAN_SYMBOLS / 2.0; k++)
|
||||
{
|
||||
if(k >= 0 && k < dibits.size())
|
||||
{
|
||||
double pulse = getRaisedCosine(t - k);
|
||||
sumI += symbolI[k] * pulse;
|
||||
sumQ += symbolQ[k] * pulse;
|
||||
}
|
||||
}
|
||||
|
||||
double carrier = 2.0 * Math.PI * carrierOffset * n / SAMPLE_RATE;
|
||||
double cos = Math.cos(carrier);
|
||||
double sin = Math.sin(carrier);
|
||||
i[n] = (float)(sumI * cos - sumQ * sin + noise * random.nextGaussian());
|
||||
q[n] = (float)(sumI * sin + sumQ * cos + noise * random.nextGaussian());
|
||||
}
|
||||
|
||||
return new float[][]{i, q};
|
||||
}
|
||||
|
||||
/**
|
||||
* Raised cosine pulse value
|
||||
* @param t time in symbol periods
|
||||
*/
|
||||
private static double getRaisedCosine(double t)
|
||||
{
|
||||
double sinc = Math.abs(t) < 1e-9 ? 1.0 : Math.sin(Math.PI * t) / (Math.PI * t);
|
||||
double denominator = 1.0 - Math.pow(2.0 * ROLLOFF * t, 2.0);
|
||||
|
||||
if(Math.abs(denominator) < 1e-9)
|
||||
{
|
||||
return Math.PI / 4.0 * sinc;
|
||||
}
|
||||
|
||||
return sinc * Math.cos(Math.PI * ROLLOFF * t) / denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the most significant bits first, as dibits.
|
||||
*/
|
||||
private static void addBits(List<Dibit> dibits, long value, int bitCount)
|
||||
{
|
||||
for(int bit = bitCount - 2; bit >= 0; bit -= 2)
|
||||
{
|
||||
dibits.add(DIBITS[(int)((value >> bit) & 3)]);
|
||||
}
|
||||
}
|
||||
|
||||
private record Result(int valid, int invalid, int first, int missed, int replayValid) {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase2.enumeration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* JUnit tests for the ISCH (super frame fragment) sequence.
|
||||
*/
|
||||
public class ISCHSequenceTest
|
||||
{
|
||||
@Test
|
||||
public void advanceCyclesThroughFragments()
|
||||
{
|
||||
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_1.advance(1));
|
||||
assertEquals(ISCHSequence.ISCH_3, ISCHSequence.ISCH_1.advance(2));
|
||||
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_1.advance(3));
|
||||
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_3.advance(1));
|
||||
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_3.advance(11));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advanceOfNonFragmentSequenceIsUnknown()
|
||||
{
|
||||
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.RESERVED_4.advance(1));
|
||||
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.UNKNOWN.advance(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase2.message;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
|
||||
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;
|
||||
|
||||
/**
|
||||
* JUnit tests for the P25 Phase 2 Encryption Synchronization Sequence (ESS) RS(44,16,29) decoding.
|
||||
*/
|
||||
public class EncryptionSynchronizationSequenceProcessorTest
|
||||
{
|
||||
/**
|
||||
* Creates the RS codeword for the 16 ESS information symbols, where codeword index 43 is the first information
|
||||
* symbol, indices 28-43 are information symbols and indices 0-27 are parity symbols.
|
||||
*/
|
||||
private static int[] encode(int[] information)
|
||||
{
|
||||
int[] message = new int[35];
|
||||
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
message[15 - x] = information[x];
|
||||
}
|
||||
|
||||
return ReedSolomonTestEncoder.encode(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* ESS-A fragment (168 bits) containing the 28 parity symbols, where hexbit x is codeword index 27 - x.
|
||||
*/
|
||||
private static BinaryMessage getEssA(int[] codeword)
|
||||
{
|
||||
BinaryMessage essA = new BinaryMessage(168);
|
||||
|
||||
for(int x = 0; x < 28; x++)
|
||||
{
|
||||
essA.load(x * 6, 6, codeword[27 - x]);
|
||||
}
|
||||
|
||||
return essA;
|
||||
}
|
||||
|
||||
/**
|
||||
* ESS-B fragment (24 bits) containing 4 information symbols, where fragment 1 is codeword indices 40-43 and
|
||||
* fragment 4 is codeword indices 28-31, and hexbit x is codeword index (base + 3 - x).
|
||||
*/
|
||||
private static BinaryMessage getEssB(int[] codeword, int fragment)
|
||||
{
|
||||
int base = 28 + (4 - fragment) * 4;
|
||||
BinaryMessage essB = new BinaryMessage(24);
|
||||
|
||||
for(int x = 0; x < 4; x++)
|
||||
{
|
||||
essB.load(x * 6, 6, codeword[base + 3 - x]);
|
||||
}
|
||||
|
||||
return essB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expected decoded 96-bit ESS message.
|
||||
*/
|
||||
private static String getExpected(int[] information)
|
||||
{
|
||||
BinaryMessage expected = new BinaryMessage(96);
|
||||
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
expected.load(x * 6, 6, information[x]);
|
||||
}
|
||||
|
||||
return expected.toHexString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrupts the specified number of distinct ESS-A (parity) symbols.
|
||||
*/
|
||||
private static void corruptParity(int[] codeword, int errors, Random random)
|
||||
{
|
||||
boolean[] corrupted = new boolean[28];
|
||||
int count = 0;
|
||||
|
||||
while(count < errors)
|
||||
{
|
||||
int index = random.nextInt(28);
|
||||
|
||||
if(!corrupted[index])
|
||||
{
|
||||
corrupted[index] = true;
|
||||
codeword[index] ^= 1 + random.nextInt(63);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] getEncryptedInformation(Random random)
|
||||
{
|
||||
int[] information = new int[16];
|
||||
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
information[x] = random.nextInt(64);
|
||||
}
|
||||
|
||||
return information;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodesCompleteSequenceWithErrors()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
|
||||
for(int trial = 0; trial < 100; trial++)
|
||||
{
|
||||
int[] information = getEncryptedInformation(random);
|
||||
int[] codeword = encode(information);
|
||||
int[] received = codeword.clone();
|
||||
corruptParity(received, 14, random);
|
||||
|
||||
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||
processor.setESSA(getEssA(received));
|
||||
processor.setESSB1(getEssB(received, 1));
|
||||
processor.setESSB2(getEssB(received, 2));
|
||||
processor.setESSB3(getEssB(received, 3));
|
||||
processor.setESSB4(getEssB(received, 4));
|
||||
|
||||
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||
assertNotNull(sequence);
|
||||
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypted sequence (random message indicator) with 3 missing ESS-B fragments and 4 parity symbol errors. The
|
||||
* missing fragments can't be decoded as zero valued symbols (12 + 4 errors exceeds the 14 symbol error capacity),
|
||||
* but can be decoded as erasures (12 erasures + 4 errors).
|
||||
*/
|
||||
@Test
|
||||
public void decodesEncryptedSequenceWithMissingFragmentsAsErasures()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
|
||||
for(int trial = 0; trial < 100; trial++)
|
||||
{
|
||||
int[] information = getEncryptedInformation(random);
|
||||
int[] codeword = encode(information);
|
||||
int[] received = codeword.clone();
|
||||
corruptParity(received, 4, random);
|
||||
|
||||
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||
processor.setESSA(getEssA(received));
|
||||
processor.setESSB1(getEssB(received, 1));
|
||||
|
||||
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||
assertNotNull(sequence, "Trial " + trial);
|
||||
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unencrypted sequence (algorithm 0x80 with zero key ID and message indicator) with 3 missing ESS-B fragments and
|
||||
* 9 parity symbol errors. The missing fragments decode as zero valued symbols (9 errors), but can't be decoded
|
||||
* as erasures (12 erasures + 9 errors exceeds the capacity).
|
||||
*/
|
||||
@Test
|
||||
public void decodesUnencryptedSequenceWithMissingFragmentsAsZeros()
|
||||
{
|
||||
Random random = new Random(3);
|
||||
int[] information = new int[16];
|
||||
information[0] = 0x20; //Algorithm 0x80 (unencrypted) in the first 8 bits
|
||||
|
||||
for(int trial = 0; trial < 100; trial++)
|
||||
{
|
||||
int[] codeword = encode(information);
|
||||
int[] received = codeword.clone();
|
||||
corruptParity(received, 9, random);
|
||||
|
||||
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||
processor.setESSA(getEssA(received));
|
||||
processor.setESSB1(getEssB(received, 1));
|
||||
|
||||
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||
assertNotNull(sequence, "Trial " + trial);
|
||||
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||
assertEquals(0x80, sequence.getAlgorithmId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase2.message.isch;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
/**
|
||||
* JUnit tests for the I-ISCH (40,9,16) codeword decoder.
|
||||
*/
|
||||
public class ISCHDecoderTest
|
||||
{
|
||||
private static final long CODE_WORD_OFFSET = 0x184229d461L;
|
||||
private final ISCHDecoder mDecoder = new ISCHDecoder();
|
||||
|
||||
/**
|
||||
* Creates the transmitted 40-bit codeword for the 9-bit message value.
|
||||
*/
|
||||
private static BinaryMessage encode(int value)
|
||||
{
|
||||
BinaryMessage message = new BinaryMessage(9);
|
||||
message.load(0, 9, value);
|
||||
BinaryMessage codeword = new BinaryMessage(40);
|
||||
codeword.load(0, 40, ISCHDecoder.getCodeWord(message) ^ CODE_WORD_OFFSET);
|
||||
return codeword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel number (1 or 2) for the message value, or 0 for an ISCH location (3) that isn't used for I-ISCH.
|
||||
*/
|
||||
private static int getChannel(int value)
|
||||
{
|
||||
int ischLocation = (value >> 3) & 0x3;
|
||||
return ischLocation == 3 ? 0 : ((value >> 5) & 0x3) + 1;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctsUpToSevenBitErrors()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
|
||||
for(int value = 0; value < 512; value++)
|
||||
{
|
||||
int channel = getChannel(value);
|
||||
|
||||
if(channel != 1 && channel != 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for(int errors = 0; errors <= 7; errors++)
|
||||
{
|
||||
for(int trial = 0; trial < 10; trial++)
|
||||
{
|
||||
BinaryMessage codeword = encode(value);
|
||||
int flipped = 0;
|
||||
|
||||
while(flipped < errors)
|
||||
{
|
||||
int bit = random.nextInt(40);
|
||||
|
||||
if(codeword.get(bit))
|
||||
{
|
||||
codeword.clear(bit);
|
||||
}
|
||||
else
|
||||
{
|
||||
codeword.set(bit);
|
||||
}
|
||||
|
||||
flipped++;
|
||||
}
|
||||
|
||||
CorrectedBinaryMessage decoded = mDecoder.decode(codeword, channel);
|
||||
assertEquals(value, decoded.getInt(0, 8), "Value [" + value + "] errors [" + errors + "]");
|
||||
assertTrue(decoded.getCorrectedBitCount() <= errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsWrongTimeslotCodewords()
|
||||
{
|
||||
for(int value = 0; value < 512; value++)
|
||||
{
|
||||
int channel = getChannel(value);
|
||||
|
||||
if(channel == 1 || channel == 2)
|
||||
{
|
||||
//A codeword for the other timeslot is at least 16 bits from every codeword for the expected timeslot
|
||||
CorrectedBinaryMessage decoded = mDecoder.decode(encode(value), channel == 1 ? 2 : 1);
|
||||
assertTrue(decoded.getCorrectedBitCount() >= 8, "Value [" + value + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase2.sync;
|
||||
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* JUnit tests for the P25 Phase 2 soft sync detector scalar and SIMD vector implementations.
|
||||
*/
|
||||
public class P25P2SoftSyncDetectorTest
|
||||
{
|
||||
private static final float IDEAL_SCORE = (float)(20 * Math.pow(3.0 * Math.PI / 4.0, 2));
|
||||
|
||||
private static P25P2SoftSyncDetector[] getDetectors()
|
||||
{
|
||||
return new P25P2SoftSyncDetector[]{new P25P2SoftSyncDetectorScalar(), new P25P2SoftSyncDetectorVector64(),
|
||||
new P25P2SoftSyncDetectorVector128(), new P25P2SoftSyncDetectorVector256(),
|
||||
new P25P2SoftSyncDetectorVector512()};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsSyncPattern()
|
||||
{
|
||||
for(P25P2SoftSyncDetector detector : getDetectors())
|
||||
{
|
||||
float[] pattern = detector.getPatternSymbols();
|
||||
float score = 0;
|
||||
|
||||
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
|
||||
for(int repeat = 0; repeat < 3; repeat++)
|
||||
{
|
||||
for(float symbol : pattern)
|
||||
{
|
||||
score = detector.process(symbol);
|
||||
}
|
||||
|
||||
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vectorImplementationsMatchScalar()
|
||||
{
|
||||
P25P2SoftSyncDetector[] detectors = getDetectors();
|
||||
Random random = new Random(1);
|
||||
|
||||
for(int x = 0; x < 1000; x++)
|
||||
{
|
||||
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
|
||||
float expected = detectors[0].process(symbol);
|
||||
|
||||
for(int d = 1; d < detectors.length; d++)
|
||||
{
|
||||
assertEquals(expected, detectors[d].process(symbol), 0.001,
|
||||
detectors[d].getClass().getSimpleName() + " at symbol " + x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user