From 9a6dc2b0a0a8ba040c692cb6b4ebc7b4eb3e5434 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Thu, 29 Oct 2026 07:32:17 -0400 Subject: [PATCH] #2524 P25P2 Decoder enhancements (#2525) * 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 --- .../c4fm/DQPSKDifferentialDemodulator.java} | 28 +- .../pll/FrequencyCorrectionSyncMonitor.java | 110 ---- .../github/dsheirer/dsp/symbol/FrameSync.java | 48 -- .../edac/ReedSolomonErasureDecoder.java | 372 ++++++++++++ .../edac/ReedSolomon_44_16_29_P25.java | 38 -- .../dsheirer/gui/viewer/P25P2Viewer.java | 3 +- .../decode/p25/phase1/P25P1DecoderLSM.java | 3 +- .../p25/phase1/P25P1SymbolProcessorLSM.java | 2 +- .../sync/P25P1SoftSyncDetectorVector512.java | 35 +- .../p25/phase1/sync/P25P1SyncDetector.java | 9 +- .../decode/p25/phase2/P25P2Decoder.java | 176 ------ .../decode/p25/phase2/P25P2DecoderHDQPSK.java | 303 +++++++--- .../decode/p25/phase2/P25P2MessageFramer.java | 122 ++-- .../p25/phase2/P25P2SuperFrameDetector.java | 307 +++++++--- .../p25/phase2/P25P2SymbolProcessor.java | 236 ++++++++ .../decode/p25/phase2/P25P2SyncDetector.java | 173 ------ .../p25/phase2/enumeration/ISCHSequence.java | 18 +- ...ptionSynchronizationSequenceProcessor.java | 108 ++-- .../phase2/message/SuperFrameFragment.java | 91 ++- .../p25/phase2/message/isch/ISCHDecoder.java | 103 ++-- .../phase2/message/mac/MacMessageFactory.java | 45 +- .../phase2/sync/P25P2SoftSyncDetector.java | 79 +++ .../sync/P25P2SoftSyncDetectorFactory.java | 50 ++ .../sync/P25P2SoftSyncDetectorScalar.java | 45 ++ .../sync/P25P2SoftSyncDetectorVector128.java | 51 ++ .../sync/P25P2SoftSyncDetectorVector256.java | 62 ++ .../sync/P25P2SoftSyncDetectorVector512.java | 60 ++ .../sync/P25P2SoftSyncDetectorVector64.java | 51 ++ .../timeslot/AbstractSignalingTimeslot.java | 261 ++++++++- .../p25/phase2/timeslot/FacchTimeslot.java | 152 ++--- .../p25/phase2/timeslot/LcchTimeslot.java | 166 ++---- .../p25/phase2/timeslot/SacchTimeslot.java | 164 ++---- .../p25/phase2/timeslot/TimeslotFactory.java | 31 +- .../vector/calibrate/CalibrationManager.java | 4 +- .../vector/calibrate/CalibrationType.java | 13 +- .../PackedSampleConverterCalibration.java | 10 +- ...InterleavedSampleConverterCalibration.java | 22 +- .../UnpackedSampleConverterCalibration.java | 22 +- .../sync/DMRSoftSyncCalibration.java | 25 +- .../sync/NXDNSoftSyncCalibration.java | 23 +- .../sync/P25P1SoftSyncCalibration.java | 25 +- .../sync/P25P2SoftSyncCalibration.java | 305 ++++++++++ .../DQPSKDifferentialDemodulatorTest.java} | 20 +- .../edac/ReedSolomonErasureDecoderTest.java | 181 ++++++ .../dsheirer/edac/ReedSolomonTestEncoder.java | 100 ++++ .../p25/phase1/P25P1DecoderLSMTest.java | 3 +- .../sync/P25P1SoftSyncDetectorTest.java | 80 +++ .../p25/phase2/P25P2DecoderHDQPSKTest.java | 548 ++++++++++++++++++ .../phase2/enumeration/ISCHSequenceTest.java | 47 ++ ...nSynchronizationSequenceProcessorTest.java | 211 +++++++ .../phase2/message/isch/ISCHDecoderTest.java | 119 ++++ .../sync/P25P2SoftSyncDetectorTest.java | 80 +++ 52 files changed, 4026 insertions(+), 1314 deletions(-) rename src/main/java/io/github/dsheirer/{module/decode/p25/phase1/P25P1DifferentialDemodulatorLSM.java => dsp/c4fm/DQPSKDifferentialDemodulator.java} (92%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/FrameSync.java create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomonErasureDecoder.java delete mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java delete mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SymbolProcessor.java delete mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorScalar.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector128.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector256.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector512.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector64.java create mode 100644 src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P2SoftSyncCalibration.java rename src/test/java/io/github/dsheirer/{module/decode/p25/phase1/P25P1DifferentialDemodulatorLSMTest.java => dsp/c4fm/DQPSKDifferentialDemodulatorTest.java} (93%) create mode 100644 src/test/java/io/github/dsheirer/edac/ReedSolomonErasureDecoderTest.java create mode 100644 src/test/java/io/github/dsheirer/edac/ReedSolomonTestEncoder.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorTest.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKTest.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequenceTest.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessorTest.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoderTest.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorTest.java diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSM.java b/src/main/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulator.java similarity index 92% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSM.java rename to src/main/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulator.java index 5ac16507..3290229e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSM.java +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulator.java @@ -17,15 +17,15 @@ * **************************************************************************** */ -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.filter.fir.real.IRealFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; /** - * P25 Phase 1 LSM (CQPSK) differential demodulator that produces a real valued sample stream for the C4FM symbol - * processor. Each output sample is the phase change across the preceding symbol period, in radians per sample (ie + * Differential QPSK (ie P25 Phase 1 LSM/CQPSK and P25 Phase 2 HDQPSK) demodulator that produces a real valued sample + * stream for the C4FM symbol processor. Each output sample is the phase change across the preceding symbol period, in radians per sample (ie * the symbol phase divided by the samples per symbol), which is the same domain as the C4FM decoder's FM demodulated * and receive filtered sample stream. * @@ -44,8 +44,8 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; * which isn't biased by the spectral tilt of a frequency selective (ie simulcast multipath) channel the way that a * spectral centroid (ie mean discriminator output) estimator is. * - * The fourth power estimator is ambiguous modulo PI/2 radians per symbol (1200 Hz at 4800 baud), so the estimator only - * resolves offsets within +/- 600 Hz. When a signal is present (ie the fourth power coherence exceeds the noise + * The fourth power estimator is ambiguous modulo PI/2 radians per symbol (1200 Hz at 4800 baud, 1500 Hz at 6000 baud), + * so the estimator only resolves offsets within +/- 1/8 of the symbol rate (ie +/- 600 Hz at 4800 baud). When a signal is present (ie the fourth power coherence exceeds the noise * threshold) but the symbol processor is unable to synchronize, the estimate is stepped by +/- PI/2 radians per symbol * to search the adjacent offset ranges. An offset just beyond the range aliases to an estimate near the opposite edge * of the range (ie -650 Hz estimates as +550 Hz), so the search first steps toward the adjacent range nearest to the @@ -53,10 +53,10 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; * same at each ambiguous offset, so high coherence without synchronization indicates an incorrect offset range, while * low coherence indicates that there is no signal. The search holds the current offset range while there is no signal * (ie between transmissions on a traffic channel) so that the next transmission starts with the offset range that last - * synchronized. Once an offset range is verified by a valid NID decode (see offsetVerified()), the search uses a - * longer dwell period before stepping away from it. Symbol processor synchronization alone doesn't verify the offset - * range since a false sync detection in an incorrect offset range would otherwise hold the search in that range for the - * longer dwell period. + * synchronized. Once an offset range is verified by the decoder (ie a valid P25 Phase 1 NID or a valid P25 Phase 2 + * MAC message, see offsetVerified()), the search uses a longer dwell period before stepping away from it. Symbol + * processor synchronization alone doesn't verify the offset range since a false sync detection in an incorrect offset + * range would otherwise hold the search in that range for the longer dwell period. * * Soft symbol reliability: the demodulated phase change across the symbol period is the difference between the phases * of the samples at either end of the period, and the phase noise variance of each sample is inversely proportional to @@ -71,7 +71,7 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; * represents the residual offset. Use getFrequencyOffset() to include the coarse offset when reporting the carrier * offset to the tuner. */ -public class P25P1DifferentialDemodulatorLSM +public class DQPSKDifferentialDemodulator { private static final double TWO_PI = 2.0 * Math.PI; //Carrier offset ambiguity step of the fourth power estimator, in radians per symbol @@ -118,7 +118,7 @@ public class P25P1DifferentialDemodulatorLSM private int mSearchIndex = 0; private int mSearchDirection = 1; private double mUnsynchronizedSignalSymbols = 0; - //Indicates the current search offset range was verified by a valid NID decode since it was selected + //Indicates the current search offset range was verified by the decoder since it was selected private boolean mSearchIndexVerified = false; private double mAverageCoherence = 0.0; //Block length in samples and the fourth power accumulation for the block under accumulation @@ -130,7 +130,7 @@ public class P25P1DifferentialDemodulatorLSM /** * Constructs an instance */ - public P25P1DifferentialDemodulatorLSM() + public DQPSKDifferentialDemodulator() { } @@ -172,8 +172,8 @@ public class P25P1DifferentialDemodulatorLSM } /** - * Notification that a valid NID was decoded, verifying the current carrier offset range so that the ambiguity search - * uses the longer dwell period before stepping away from it. + * Notification that the decoder verified the current carrier offset range (ie a valid NID or MAC message was + * decoded) so that the ambiguity search uses the longer dwell period before stepping away from it. */ public void offsetVerified() { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java deleted file mode 100644 index b02f04fe..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2025 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 - * **************************************************************************** - */ -package io.github.dsheirer.dsp.psk.pll; - -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; -import io.github.dsheirer.module.decode.FeedbackDecoder; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Monitors decode sync events to adaptively control frequency correction broadcasts. - */ -@Deprecated //Remove after P25P2 decoder gets updated -public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFrequencyErrorProcessor -{ - private final static Logger mLog = LoggerFactory.getLogger(FrequencyCorrectionSyncMonitor.class); - - private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd(); - private FeedbackDecoder mFeedbackDecoder; - private CostasLoop mCostasLoop; - private int mSyncCount; - - /** - * Constructs an adaptive monitor to monitor the sync state of a decoder - * - * @param costasLoop to receive adaptive gain updates. - * @param feedbackDecoder to rebroadcast frequency error source events - */ - public FrequencyCorrectionSyncMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder) - { - mCostasLoop = costasLoop; - mCostasLoop.setFrequencyErrorProcessor(this); - mFeedbackDecoder = feedbackDecoder; - } - - /** - * Sync detection event. Updates the running sync count and updates the PLL gain level. - */ - @Override - public void syncDetected(int bitErrors) - { - mSyncCount++; - update(); - } - - /** - * Sync loss event. Updates the running sync count and updates the PLL gain level. - */ - @Override - public void syncLost(int bitsProcessed) - { - mSyncCount -= 2; - update(); - } - - /** - * Updates the sync count - */ - private void update() - { - if(mSyncCount < 0) - { - mSyncCount = 0; - return; - } - - if(mSyncCount > MAX_SYNC_COUNT) - { - mSyncCount = MAX_SYNC_COUNT; - return; - } - } - - /** - * Resets the monitor - */ - public void reset() - { - mSyncCount = 0; - } - - @Override - public void processFrequencyError(long frequencyError) - { - mFeedbackDecoder.broadcast(SourceEvent.carrierOffsetMeasurement(-frequencyError)); - - //Only rebroadcast as a frequency error measurement if the sync count is more than 2 - if(mSyncCount > 2) - { - mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError)); - } - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/FrameSync.java b/src/main/java/io/github/dsheirer/dsp/symbol/FrameSync.java deleted file mode 100644 index 4ab0df43..00000000 --- a/src/main/java/io/github/dsheirer/dsp/symbol/FrameSync.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.dsp.symbol; - -public enum FrameSync -{ - P25_PHASE1_ERROR_90_CCW( 0xFFEFAFAAEEAAl ), - P25_PHASE1_NORMAL( 0x5575F5FF77FFl ), // +33333 -3 +33 -33 +33 -3333 +3 -3 +3 -33333 - P25_PHASE1_ERROR_90_CW( 0x001050551155l ), - P25_PHASE1_ERROR_180( 0xAA8A0A008800l ), - - P25_PHASE2_NORMAL( 0x575D57F7FFl), - P25_PHASE2_ERROR_90_CCW(0xFEFBFEAEAAl), - P25_PHASE2_ERROR_90_CW( 0x0104015155l), - P25_PHASE2_ERROR_180( 0xA8A2A80800l); - - private long mSync; - - FrameSync( long sync ) - { - mSync = sync; - } - - public long getSync() - { - return mSync; - } -} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomonErasureDecoder.java b/src/main/java/io/github/dsheirer/edac/ReedSolomonErasureDecoder.java new file mode 100644 index 00000000..0b27df0b --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomonErasureDecoder.java @@ -0,0 +1,372 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.edac; + +/** + * Reed-Solomon errors-and-erasures decoder. Corrects any combination of v errors and e erasures (symbols at known + * positions with unknown values) where 2v + e <= n - k. Erasures support decoding of punctured codes (ie the punctured + * parity symbols are erasures instead of errors) and soft decision (ie generalized minimum distance) decoding where the + * least reliable received symbols are erased. + * + * Uses the same code conventions as the BerlekempMassey decoder: symbols are in polynomial form, symbol index j is the + * coefficient of x^j, the generator polynomial roots are alpha^1 through alpha^(n-k), and the code is systematic with the + * parity symbols at indices 0 to (n-k-1) and the message symbols at indices (n-k) to (n-1). + * + * Algorithm: computes the syndromes, the erasure locator polynomial and the modified (Forney) syndromes, applies the + * Berlekamp-Massey algorithm to the modified syndromes to find the error locator polynomial, combines the error and + * erasure locators into the errata locator, then uses a Chien search to find the errata locations and the Forney + * algorithm to calculate the errata values. The corrected codeword is verified to have zero syndromes. + */ +public class ReedSolomonErasureDecoder +{ + private final int mN; + private final int mParityCount; + private final int mFieldSize; + private final int[] mExp; + private final int[] mLog; + + /** + * Constructs an instance + * @param bitSize of each symbol (ie 6 for GF(2^6)) + * @param n codeword length in symbols + * @param k message length in symbols + * @param generatorPolynomial for the Galois field in big-endian form (see BerlekempMassey) + */ + public ReedSolomonErasureDecoder(int bitSize, int n, int k, int[] generatorPolynomial) + { + mN = n; + mParityCount = n - k; + mFieldSize = (1 << bitSize) - 1; + mExp = new int[mFieldSize * 2]; + mLog = new int[mFieldSize + 1]; + + int primitive = 0; + + for(int i = 0; i < bitSize; i++) + { + if(generatorPolynomial[i] != 0) + { + primitive |= (1 << i); + } + } + + int value = 1; + + for(int i = 0; i < mFieldSize; i++) + { + mExp[i] = value; + mExp[i + mFieldSize] = value; + mLog[value] = i; + value <<= 1; + + if(value > mFieldSize) + { + value = (value & mFieldSize) ^ primitive; + } + } + } + + /** + * APCO-25 RS(63,35,29) decoder used for P25 Phase 2 FACCH, SACCH and LCCH signaling. + */ + public static ReedSolomonErasureDecoder p25_63_35_29() + { + return new ReedSolomonErasureDecoder(6, 63, 35, ReedSolomon_63_P25.P25_GENERATOR_POLYNOMIAL); + } + + /** + * Maximum number of erasures (with zero errors) that can be corrected. + */ + public int getParityCount() + { + return mParityCount; + } + + private int multiply(int a, int b) + { + if(a == 0 || b == 0) + { + return 0; + } + + return mExp[mLog[a] + mLog[b]]; + } + + private int divide(int a, int b) + { + if(a == 0) + { + return 0; + } + + return mExp[mLog[a] - mLog[b] + mFieldSize]; + } + + /** + * Alpha raised to the power, for any non-negative power. + */ + private int alpha(int power) + { + return mExp[power % mFieldSize]; + } + + /** + * Evaluates the polynomial (coefficients in ascending order) at x. + */ + private int evaluate(int[] polynomial, int length, int x) + { + int result = 0; + + for(int i = length - 1; i >= 0; i--) + { + result = multiply(result, x) ^ polynomial[i]; + } + + return result; + } + + /** + * Calculates the syndromes S1 through S(n-k) into syndromes[0 .. n-k-1]. + * @return true if any syndrome is non-zero. + */ + private boolean syndromes(int[] codeword, int[] syndromes) + { + boolean error = false; + + for(int i = 0; i < mParityCount; i++) + { + int s = 0; + int x = alpha(i + 1); + + for(int j = mN - 1; j >= 0; j--) + { + s = multiply(s, x) ^ codeword[j]; + } + + syndromes[i] = s; + error |= (s != 0); + } + + return error; + } + + /** + * Decodes the received codeword with the specified erasure positions. + * + * @param input received codeword symbols (polynomial form) of length n. Values at the erasure positions are ignored. + * @param output receives the corrected codeword, or a copy of the input (with zeros at the erasure positions) when + * the codeword can't be corrected. + * @param erasures positions of the erased symbols + * @param erasureCount number of erasure positions to use from the erasures array + * @return true if the codeword has irrecoverable errors, consistent with the BerlekempMassey decoder. + */ + public boolean decode(int[] input, int[] output, int[] erasures, int erasureCount) + { + for(int j = 0; j < mN; j++) + { + output[j] = input[j]; + } + + for(int x = 0; x < erasureCount; x++) + { + output[erasures[x]] = 0; + } + + if(erasureCount > mParityCount) + { + return true; + } + + int[] syndromes = new int[mParityCount]; + + if(!syndromes(output, syndromes)) + { + return false; + } + + int length = mParityCount + 1; + + //Erasure locator: product of (1 + X x) for each erasure location X = alpha^position + int[] erasureLocator = new int[length]; + erasureLocator[0] = 1; + + for(int x = 0; x < erasureCount; x++) + { + int location = alpha(erasures[x]); + + for(int i = x + 1; i > 0; i--) + { + erasureLocator[i] ^= multiply(erasureLocator[i - 1], location); + } + } + + //Modified (Forney) syndromes: erasure locator x syndrome polynomial, modulo x^(n-k) + int[] modified = new int[mParityCount]; + + for(int i = 0; i < mParityCount; i++) + { + int value = 0; + + for(int j = 0; j <= Math.min(i, erasureCount); j++) + { + value ^= multiply(erasureLocator[j], syndromes[i - j]); + } + + modified[i] = value; + } + + //Berlekamp-Massey on the modified syndromes beyond the erasure count to find the error locator + int[] errorLocator = new int[length]; + int[] previous = new int[length]; + int[] temp = new int[length]; + errorLocator[0] = 1; + previous[0] = 1; + int errorCount = 0; + int shift = 1; + int previousDiscrepancy = 1; + int sequenceLength = mParityCount - erasureCount; + + for(int n = 0; n < sequenceLength; n++) + { + int discrepancy = modified[erasureCount + n]; + + for(int i = 1; i <= errorCount; i++) + { + discrepancy ^= multiply(errorLocator[i], modified[erasureCount + n - i]); + } + + if(discrepancy == 0) + { + shift++; + } + else + { + int scale = divide(discrepancy, previousDiscrepancy); + System.arraycopy(errorLocator, 0, temp, 0, length); + + for(int i = 0; i + shift < length; i++) + { + errorLocator[i + shift] ^= multiply(scale, previous[i]); + } + + if(2 * errorCount <= n) + { + errorCount = n + 1 - errorCount; + System.arraycopy(temp, 0, previous, 0, length); + previousDiscrepancy = discrepancy; + shift = 1; + } + else + { + shift++; + } + } + } + + if(2 * errorCount + erasureCount > mParityCount) + { + return true; + } + + //Errata locator: error locator x erasure locator + int errataDegree = errorCount + erasureCount; + int[] errataLocator = new int[length]; + + for(int i = 0; i <= errorCount; i++) + { + if(errorLocator[i] != 0) + { + for(int j = 0; j <= erasureCount; j++) + { + errataLocator[i + j] ^= multiply(errorLocator[i], erasureLocator[j]); + } + } + } + + //Errata evaluator: syndrome polynomial x errata locator, modulo x^(n-k) + int[] evaluator = new int[mParityCount]; + + for(int i = 0; i < mParityCount; i++) + { + int value = 0; + + for(int j = 0; j <= Math.min(i, errataDegree); j++) + { + value ^= multiply(errataLocator[j], syndromes[i - j]); + } + + evaluator[i] = value; + } + + //Formal derivative of the errata locator (odd power terms only in GF(2^m)) + int[] derivative = new int[length]; + + for(int i = 1; i <= errataDegree; i += 2) + { + derivative[i - 1] = errataLocator[i]; + } + + //Chien search for the errata locations, with the Forney algorithm for the errata values + int rootCount = 0; + + for(int j = 0; j < mN; j++) + { + int inverse = alpha(mFieldSize - (j % mFieldSize)); + + if(evaluate(errataLocator, errataDegree + 1, inverse) == 0) + { + rootCount++; + int denominator = evaluate(derivative, errataDegree, inverse); + + if(denominator == 0) + { + restore(input, output, erasures, erasureCount); + return true; + } + + output[j] ^= divide(evaluate(evaluator, mParityCount, inverse), denominator); + } + } + + if(rootCount != errataDegree || syndromes(output, syndromes)) + { + restore(input, output, erasures, erasureCount); + return true; + } + + return false; + } + + /** + * Restores the output to the received codeword after a decoding failure. + */ + private void restore(int[] input, int[] output, int[] erasures, int erasureCount) + { + for(int j = 0; j < mN; j++) + { + output[j] = input[j]; + } + + for(int x = 0; x < erasureCount; x++) + { + output[erasures[x]] = 0; + } + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java deleted file mode 100644 index 0c8119a3..00000000 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.edac; - -/** - * Reed Solomon Decoder for P25 RS(44,16,29) protected messages. - */ -public class ReedSolomon_44_16_29_P25 extends ReedSolomon_63_P25 -{ - /** - * Constructs an instance - */ - public ReedSolomon_44_16_29_P25() - { - //The RS(44,16,29) is shortened from RS(63,35,29) - super(63,35); - } -} diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java index d31f86c3..4dce53b4 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java @@ -225,7 +225,8 @@ public class P25P2Viewer extends VBox ThreadPool.CACHED.submit(() -> { List messages = new ArrayList<>(); - P25P2MessageFramer messageFramer = new P25P2MessageFramer(null); + P25P2MessageFramer messageFramer = new P25P2MessageFramer(); + messageFramer.start(); messageFramer.setScrambleParameters(scrambleParameters); P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor(); messageFramer.setListener(messageProcessor); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java index 290fb33c..77e38913 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java @@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.p25.phase1; import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.c4fm.DQPSKDifferentialDemodulator; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory; import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; @@ -76,7 +77,7 @@ public class P25P1DecoderLSM extends FeedbackDecoder implements IByteBufferProvi private static final int SYMBOL_RATE = 4800; private final P25P1SymbolProcessorLSM mSymbolProcessor; - private final P25P1DifferentialDemodulatorLSM mDemodulator = new P25P1DifferentialDemodulatorLSM(); + private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator(); private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer(); private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor(); private IRealDecimationFilter mDecimationFilterI; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorLSM.java index 8488195a..6e189711 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorLSM.java @@ -24,7 +24,7 @@ import io.github.dsheirer.module.decode.FeedbackDecoder; /** * P25 Phase 1 LSM implementation of the symbol processor. Processes the differentially demodulated sample stream - * produced by the P25P1DifferentialDemodulatorLSM, which is in the same radians per sample domain as the C4FM decoder's + * produced by the DQPSKDifferentialDemodulator, which is in the same radians per sample domain as the C4FM decoder's * sample stream, so this processor shares the P25 C4FM sync detection, optimization and tuning parameters. * * Note: testing against LSM recordings without injected noise showed no meaningful difference across equalizer spans diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java index 93aee1a8..9482610d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -20,7 +20,6 @@ package io.github.dsheirer.module.decode.p25.phase1.sync; import jdk.incubator.vector.FloatVector; -import jdk.incubator.vector.VectorMask; import jdk.incubator.vector.VectorOperators; import jdk.incubator.vector.VectorSpecies; @@ -29,27 +28,33 @@ import jdk.incubator.vector.VectorSpecies; */ public class P25P1SoftSyncDetectorVector512 extends P25P1SoftSyncDetector { - private static final boolean[] VECTOR_MASK = new boolean[]{false, false, false, false, false, false, false, false, - true, true, true, true, true, true, true, true}; private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; + //Final pattern symbols, zero padded to the vector length, for the final (overlapping) vector of symbols + private final float[] mTailPatternSymbols = new float[16]; /** - * Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft - * symbols that include this recent soft symbol. + * Constructs an instance + */ + public P25P1SoftSyncDetectorVector512() + { + System.arraycopy(getPatternSymbols(), 16, mTailPatternSymbols, 8, 8); + } + + /** + * Calculates the correlation of the preceding 24 soft symbols against the sync pattern. * * @return correlation score. */ + @Override public float calculate() { - //There are 24 symbols that we load into 2x vectors of 16. We load the first 16 symbols into vector 1 and the - //remaining 8 symbols into vector 2 using a vector mask that ignores the first 8 symbols and loads the second 8 - // symbols. This sets the first 8 symbols to zero and those values are ignored during calculations. - VectorMask mask = VectorMask.fromArray(VECTOR_SPECIES, VECTOR_MASK, 0); - FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); - accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer) - .fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0), accumulator); - accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8, mask) - .fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 8), accumulator); + //There are 24 symbols that we load into 2x vectors of 16. The second vector overlaps the first vector by 8 + //symbols (8-15), so it is multiplied against the final 8 pattern symbols (16-23) zero padded in the first 8 + //lanes. Multiplying against zero is cheaper than a masked load. + FloatVector accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer) + .mul(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0)); + accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8) + .fma(FloatVector.fromArray(VECTOR_SPECIES, mTailPatternSymbols, 0), accumulator); return accumulator.reduceLanes(VectorOperators.ADD); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java index 9ec7767a..be222c1a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * 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 @@ -26,11 +26,16 @@ import io.github.dsheirer.dsp.c4fm.DibitSyncDetector; */ public abstract class P25P1SyncDetector extends DibitSyncDetector { + /** + * P25 Phase 1 sync pattern: +33333 -3 +33 -33 +33 -3333 +3 -3 +3 -33333 + */ + public static final long SYNC_PATTERN = 0x5575F5FF77FFL; + /** * Constructs an instance */ public P25P1SyncDetector() { - super(0x5575F5FF77FFL, 24); + super(SYNC_PATTERN, 24); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java deleted file mode 100644 index 622a1ff5..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * ***************************************************************************** - * 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 - * **************************************************************************** - */ -package io.github.dsheirer.module.decode.p25.phase2; - -import com.google.common.eventbus.Subscribe; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; -import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.FeedbackDecoder; -import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent; -import io.github.dsheirer.sample.Broadcaster; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.IByteBufferProvider; -import io.github.dsheirer.sample.complex.ComplexSamples; -import io.github.dsheirer.sample.complex.IComplexSamplesListener; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.SourceEvent; -import java.nio.ByteBuffer; - -/** - * Base P25 Phase 2 Decoder - */ -public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEventListener, - IComplexSamplesListener, Listener, IByteBufferProvider -{ - private double mSampleRate; - private final Broadcaster mDibitBroadcaster = new Broadcaster<>(); - private final DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300); - private final P25P2MessageProcessor mMessageProcessor; - private double mSymbolRate; - - public P25P2Decoder(double symbolRate) - { - mSymbolRate = symbolRate; - mMessageProcessor = new P25P2MessageProcessor(); - mMessageProcessor.setMessageListener(getMessageListener()); - getDibitBroadcaster().addListener(mByteBufferAssembler); - } - - @Override - public String getProtocolDescription() - { - return "P25 Phase 2"; - } - - /** - * Preloads P25 frequency bands when this is a traffic channel, since they are not transmitted on the traffic - * channel and the decoder state needs accurate frequency values to pass to the traffic channel manager for - * call event tracking. - * @param preLoadDataContent with known frequency bands. - */ - @Subscribe - public void process(P25FrequencyBandPreloadDataContent preLoadDataContent) - { - mMessageProcessor.preload(preLoadDataContent); - } - - /** - * Assembler for packaging Dibit stream into reusable byte buffers. - */ - protected Broadcaster getDibitBroadcaster() - { - return mDibitBroadcaster; - } - - /** - * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler - */ - @Override - public void setBufferListener(Listener listener) - { - mByteBufferAssembler.setBufferListener(listener); - } - - /** - * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler - */ - @Override - public void removeBufferListener(Listener listener) - { - mByteBufferAssembler.removeBufferListener(listener); - } - - /** - * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler - */ - @Override - public boolean hasBufferListeners() - { - return mByteBufferAssembler.hasBufferListeners(); - } - - protected double getSymbolRate() - { - return mSymbolRate; - } - - /** - * Current sample rate for this decoder - */ - protected double getSampleRate() - { - return mSampleRate; - } - - /** - * Sets current sample rate for this decoder - */ - public void setSampleRate(double sampleRate) - { - if(sampleRate <= getSymbolRate() * 2) - { - throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be > 12000 (2 * " + - getSymbolRate() + " symbol rate)"); - } - - mSampleRate = sampleRate; - } - - /** - * Samples per symbol based on current sample rate and symbol rate. - */ - public float getSamplesPerSymbol() - { - return (float)(getSampleRate() / getSymbolRate()); - } - - @Override - public Listener getSourceEventListener() - { - return this::process; - } - - /** - * Sub-class processing of received source events - * - * @param sourceEvent to process - */ - protected abstract void process(SourceEvent sourceEvent); - - /** - * Listener interface to receive reusable complex buffers - */ - @Override - public Listener getComplexSamplesListener() - { - return P25P2Decoder.this; - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.P25_PHASE2; - } - - protected P25P2MessageProcessor getMessageProcessor() - { - return mMessageProcessor; - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java index 42b51983..f85c1616 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java @@ -18,24 +18,24 @@ */ package io.github.dsheirer.module.decode.p25.phase2; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.channel.state.MultiChannelState; import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.dsp.c4fm.DQPSKDifferentialDemodulator; import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory; +import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; -import io.github.dsheirer.dsp.gain.complex.ComplexGainFactory; -import io.github.dsheirer.dsp.gain.complex.IComplexGainControl; -import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; -import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; -import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor; -import io.github.dsheirer.dsp.psk.pll.PLLBandwidth; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.IdentifierUpdateListener; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.module.ProcessingChain; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent; import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager; import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; @@ -43,10 +43,14 @@ import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IByteBufferProvider; import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.sample.complex.IComplexSamplesListener; +import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.wave.ComplexWaveSource; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Iterator; @@ -54,85 +58,119 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * P25 Phase 2 HDQPSK 2-timeslot Decoder + * APCO25 Phase 2 HDQPSK 2-timeslot decoder. Decimates incoming sample buffers to 4-8 samples per symbol. Employs a + * flat baseband (channel) filter without a matched (root raised cosine) filter since the differential demodulator is a + * limiter-discriminator receiver where narrowing the bandwidth ahead of the discriminator distorts the phase trajectory. + * The differential demodulator (mDemodulator) removes the coarse carrier offset and differentially demodulates the + * filtered complex baseband (I/Q) sample stream into the phase change across each symbol period, in the same radians + * per sample domain as the C4FM decoders. This allows the decoder to share the C4FM symbol processor framework + * (mSymbolProcessor) that performs sync detection, timing correction, equalization and balance (PLL) residual mistune + * correction. The message framer frames super frame fragments and a registered message listener receives the decoded + * messages. + * + * As a child of the FeedbackDecoder, this decoder provides periodic PLL measurements to the tuner for automatic PPM + * correction. It also provides a stream of demodulated soft symbols (in radians) for display to the user. */ -public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdateListener +public class P25P2DecoderHDQPSK extends FeedbackDecoder implements ISourceEventListener, IComplexSamplesListener, + Listener, IByteBufferProvider, IdentifierUpdateListener { private final static Logger mLog = LoggerFactory.getLogger(P25P2DecoderHDQPSK.class); - protected static final float SYMBOL_TIMING_GAIN = 0.1f; - protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; - protected DQPSKGardnerDemodulator mQPSKDemodulator; - protected CostasLoop mCostasLoop; - protected P25P2MessageFramer mMessageFramer; - protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl(); - protected IRealFilter mIBasebandFilter; - protected IRealFilter mQBasebandFilter; - private DecodeConfigP25Phase2 mDecodeConfigP25Phase2; - private FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor; + //Baseband filter settings. The filter precedes the differential demodulator's carrier offset removal, so the pass + //band must accommodate the carrier offset. Testing against Phase 2 recordings (pass/stop: total valid MAC messages) + //5500/7000: 9210, 6000/7500: 9273, 6500/7800: 9294, 7000/8500: 9262. Narrower pass bands lost the most valid + //messages on a recording with a 1.8 kHz carrier offset. + private static final double BASEBAND_FILTER_PASS_BAND = 6500.0; //Hertz + private static final double BASEBAND_FILTER_STOP_BAND = 7800.0; //Hertz + private static final int SYMBOL_RATE = 6000; + private final P25P2SymbolProcessor mSymbolProcessor; + private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator(); + private final P25P2MessageFramer mMessageFramer = new P25P2MessageFramer(); + private final P25P2MessageProcessor mMessageProcessor = new P25P2MessageProcessor(); + private final DecodeConfigP25Phase2 mDecodeConfigP25Phase2; + private IRealDecimationFilter mDecimationFilterI; + private IRealDecimationFilter mDecimationFilterQ; + private IRealFilter mBasebandFilterI; + private IRealFilter mBasebandFilterQ; + /** + * Constructs an instance + * @param decodeConfigP25Phase2 with optional scramble parameters + */ public P25P2DecoderHDQPSK(DecodeConfigP25Phase2 decodeConfigP25Phase2) { - super(6000.0); - setSampleRate(25000.0); mDecodeConfigP25Phase2 = decodeConfigP25Phase2; - } + mMessageProcessor.setMessageListener(getMessageListener()); + mMessageFramer.setListener(mMessageProcessor); + mSymbolProcessor = new P25P2SymbolProcessor(this, mMessageFramer); - @Override - public void start() - { - super.start(); - mQPSKDemodulator.start(); - - //Refresh the scramble parameters each time we start in case they change - if(mDecodeConfigP25Phase2 != null && mDecodeConfigP25Phase2.getScrambleParameters() != null && - !mDecodeConfigP25Phase2.isAutoDetectScrambleParameters()) + if(mDecodeConfigP25Phase2 != null) { mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters()); } + + setSampleRate(25000.0); } @Override - public void stop() + public String getProtocolDescription() { - super.stop(); - mQPSKDemodulator.stop(); + return "P25 Phase 2"; } + @Override + public DecoderType getDecoderType() + { + return DecoderType.P25_PHASE2; + } + + /** + * Preloads P25 frequency bands when this is a traffic channel, since they are not transmitted on the traffic + * channel and the decoder state needs accurate frequency values to pass to the traffic channel manager for + * call event tracking. + * @param preLoadDataContent with known frequency bands. + */ + @Subscribe + public void process(P25FrequencyBandPreloadDataContent preLoadDataContent) + { + mMessageProcessor.preload(preLoadDataContent); + } + + /** + * Sets the sample rate and configures internal components. + * @param sampleRate of the channel to decode + */ public void setSampleRate(double sampleRate) { - super.setSampleRate(sampleRate); - - //Note: the baseband filter is designed for a 50 kHz sample rate, independent of the current sample rate. - float[] basebandTaps = FilterFactory.getRemezLowPass(50000.0, 6500, 0.005, 7200, 0.01); - mIBasebandFilter = FilterFactory.getRealFilter(basebandTaps); - mQBasebandFilter = FilterFactory.getRealFilter(basebandTaps); - mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); - mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300); - - mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); - mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - - if(mMessageFramer != null) + if(sampleRate <= SYMBOL_RATE * 2) { - getDibitBroadcaster().removeListener(mMessageFramer); + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >12000 (2 * " + + SYMBOL_RATE + " symbol rate)"); } - //The Costas Loop receives symbol-inversion correction requests when detected. - //The PLL gain monitor receives sync detect/loss signals from the message framer - mMessageFramer = new P25P2MessageFramer(mCostasLoop); + int decimation = 1; - if(mDecodeConfigP25Phase2 !=null) + //Identify decimation that gets as close to 4.0 Samples Per Symbol as possible (6000 x 4.0 = 24 kHz) + while((sampleRate / decimation) >= 48000) { - mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters()); + decimation *= 2; } - mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this); - mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor); - mMessageFramer.setListener(getMessageProcessor()); - mMessageFramer.setSampleRate(sampleRate); + mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation); + mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation); - mQPSKDemodulator.setSymbolListener(getDibitBroadcaster()); - getDibitBroadcaster().addListener(mMessageFramer); + float decimatedSampleRate = (float)sampleRate / decimation; + + //Set the decimated sample rate to use for PLL error reporting. + setDecimatedSampleRate(decimatedSampleRate); + + float[] basebandTaps = FilterFactory.getRemezLowPass(decimatedSampleRate, BASEBAND_FILTER_PASS_BAND, + BASEBAND_FILTER_STOP_BAND); + mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps); + mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps); + float samplesPerSymbol = decimatedSampleRate / (float)SYMBOL_RATE; + + mDemodulator.setSamplesPerSymbol(samplesPerSymbol); + mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol); } /** @@ -142,25 +180,118 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate @Override public void receive(ComplexSamples samples) { - float[] i = mIBasebandFilter.filter(samples.i()); - float[] q = mQBasebandFilter.filter(samples.q()); + //Update the message framer with the timestamp from the incoming sample buffer. + mMessageFramer.setTimestamp(samples.timestamp()); - ComplexSamples amplified = mAGC.process(i, q, samples.timestamp()); - mQPSKDemodulator.receive(amplified); + float[] i = mDecimationFilterI.decimateReal(samples.i()); + float[] q = mDecimationFilterQ.decimateReal(samples.q()); + + i = mBasebandFilterI.filter(i); + q = mBasebandFilterQ.filter(q); + + //Differential demodulation: phase change across each symbol period (radians per sample) + float[] demodulated = mDemodulator.demodulate(i, q, mSymbolProcessor.isSynchronized()); + + //Process demodulated samples into symbols and apply sync detection and message framing. + mSymbolProcessor.process(demodulated); + } + + /** + * Includes the coarse carrier offset that the differential demodulator removes ahead of the symbol processor, + * since the symbol processor's PLL error only represents the residual offset. The symbol processor invokes this + * method when the message framer produces a valid super frame fragment, which also verifies the demodulator's + * carrier offset range. + * @param pllError residual carrier offset (radians per sample) from the symbol processor + */ + @Override + public void processPLLError(float pllError) + { + mDemodulator.offsetVerified(); + super.processPLLError(pllError - mDemodulator.getFrequencyOffset()); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public void setBufferListener(Listener listener) + { + mSymbolProcessor.setBufferListener(listener); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public void removeBufferListener(Listener listener) + { + setBufferListener(null); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public boolean hasBufferListeners() + { + return mSymbolProcessor.hasBufferListener(); } @Override - protected void process(SourceEvent sourceEvent) + public Listener getSourceEventListener() + { + return this::process; + } + + /** + * Listener interface to receive reusable complex buffers + */ + @Override + public Listener getComplexSamplesListener() + { + return this; + } + + @Override + public void start() + { + super.start(); + + //Refresh the scramble parameters each time we start in case they change + if(mDecodeConfigP25Phase2 != null && mDecodeConfigP25Phase2.getScrambleParameters() != null && + !mDecodeConfigP25Phase2.isAutoDetectScrambleParameters()) + { + mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters()); + } + + mMessageFramer.start(); + } + + @Override + public void stop() + { + super.stop(); + mMessageFramer.stop(); + mSymbolProcessor.dispose(); + } + + /** + * Process source events + */ + private void process(SourceEvent sourceEvent) { switch(sourceEvent.getEvent()) { - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mCostasLoop.reset(); - setSampleRate(sourceEvent.getValue().doubleValue()); + case NOTIFICATION_FREQUENCY_CHANGE: + mSymbolProcessor.resetBalance(); + mDemodulator.reset(); break; case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - //Reset the PLL if/when the tuner PPM changes so that we can re-lock - mCostasLoop.reset(); + //Tuner PPM changes are small and the carrier offset estimator and symbol processor balance track the + //change, so a reset would only discard the carrier offset estimate. + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setSampleRate(sourceEvent.getValue().doubleValue()); break; } } @@ -171,22 +302,18 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate @Override public void reset() { - mCostasLoop.reset(); + mSymbolProcessor.resetBalance(); + mDemodulator.reset(); } @Override public Listener getIdentifierUpdateListener() { - return new Listener() - { - @Override - public void receive(IdentifierUpdateNotification identifierUpdateNotification) + return identifierUpdateNotification -> { + if(identifierUpdateNotification.getIdentifier().getForm() == Form.SCRAMBLE_PARAMETERS) { - if(identifierUpdateNotification.getIdentifier().getForm() == Form.SCRAMBLE_PARAMETERS && mMessageFramer != null) - { - ScrambleParameters scrambleParameters = (ScrambleParameters)identifierUpdateNotification.getIdentifier().getValue(); - mMessageFramer.setScrambleParameters(scrambleParameters); - } + ScrambleParameters scrambleParameters = (ScrambleParameters)identifierUpdateNotification.getIdentifier().getValue(); + mMessageFramer.setScrambleParameters(scrambleParameters); } }; } @@ -200,17 +327,17 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2(); channel.setDecodeConfiguration(config); - Path path = Paths.get("/home/denny/temp/20240309_054833_855787500_Duke_Energy_Monroe_Dicks_Creek_Site_201-24_1_baseband.wav"); -// ScrambleParameters scrambleParameters = new ScrambleParameters(0x91F14, 0x201, 0x00A); -// config.setScrambleParameters(scrambleParameters); + Path directory = Paths.get("/run/media/denny/T9/P25P2 Baseband Samples/"); + Path path = directory.resolve("20240303_203244_859412500_Duke_Energy_P25_Lake_Control_28_baseband.wav"); + +// ScrambleParameters pcwinScrambleParameters = new ScrambleParameters(0xBEE00, 0x3BB, 0x3B2); +// Path path = directory.resolve("20261029_043353_853425000_PCWIN_Tucson_TCH_168_baseband.wav"); +// Path path = directory.resolve("20261029_044105_851562500_PCWIN_Tucson_TCH_168_baseband.wav"); +// Path path = directory.resolve("20261029_044126_851612500_PCWIN_Tucson_TCH_289_baseband.wav"); +// Path path = directory.resolve("20261029_044556_852450000_PCWIN_Tucson_TCH_168_baseband.wav"); +// Path path = directory.resolve("20261029_044902_853175000_PCWIN_Tucson_TCH_168_baseband.wav"); -// Path path = Paths.get("/home/denny/temp/PA-STARNet_ECEN_TRAFFIC_10_baseband_20220610_104744.wav"); -// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 2370, 2369); -// config.setScrambleParameters(scrambleParameters); -// Path path = Paths.get("/home/denny/temp/20240303_203244_859412500_Duke_Energy_P25_Lake_Control_28_baseband.wav"); -// ScrambleParameters scrambleParameters = new ScrambleParameters(0x91F14, 0x2D7, 0x00A); -// config.setScrambleParameters(scrambleParameters); AliasList aliasList = new AliasList("bogus"); ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java index 8812f52a..9667ef23 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -21,9 +21,8 @@ package io.github.dsheirer.module.decode.p25.phase2; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.controller.channel.Channel; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.message.IMessage; @@ -49,52 +48,29 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors - * and issue phase corrections. + * P25 Phase 2 message framer. Receives symbol decisions (dibits) and sync detection notifications from the symbol + * processor and frames super frame fragments using the super frame detector. */ -public class P25P2MessageFramer implements Listener +public class P25P2MessageFramer extends C4FMMessageFramer { private final static Logger mLog = LoggerFactory.getLogger(P25P2MessageFramer.class); - private P25P2SuperFrameDetector mSuperFrameDetector; + private final P25P2SuperFrameDetector mSuperFrameDetector = new P25P2SuperFrameDetector(); + private boolean mRunning = false; /** * Constructs an instance - * @param phaseLockedLoop for use with the super frame detector */ - public P25P2MessageFramer(IPhaseLockedLoop phaseLockedLoop) + public P25P2MessageFramer() { - mSuperFrameDetector = new P25P2SuperFrameDetector(phaseLockedLoop); } /** * Sets or updates the scramble parameters for the current channel - * @param scrambleParameters + * @param scrambleParameters to apply */ public void setScrambleParameters(ScrambleParameters scrambleParameters) { - if(mSuperFrameDetector != null) - { - mSuperFrameDetector.setScrambleParameters(scrambleParameters); - } - } - - /** - * Sets the sample rate for the sync detector - */ - public void setSampleRate(double sampleRate) - { - mSuperFrameDetector.setSampleRate(sampleRate); - } - - /** - * Registers a sync detect listener to be notified each time sync is detected or lost. - * - * Note: this actually registers the listener on the enclosed super frame detector which has access to the - * actual sync pattern detector instance. - */ - public void setSyncDetectListener(ISyncDetectListener listener) - { - mSuperFrameDetector.setSyncDetectListener(listener); + mSuperFrameDetector.setScrambleParameters(scrambleParameters); } /** @@ -104,22 +80,87 @@ public class P25P2MessageFramer implements Listener */ public void setListener(Listener messageListener) { - mSuperFrameDetector.setListener(messageListener); + mSuperFrameDetector.setListener(message -> { + if(mRunning) + { + messageListener.receive(message); + } + }); + } + + /** + * Sets or updates the current dibit stream time from an incoming sample buffer. + * @param timestamp to use as a reference timestamp. + */ + public void setTimestamp(long timestamp) + { + mSuperFrameDetector.setTimestamp(timestamp); + } + + /** + * Starts this framer dispatching messages + */ + public void start() + { + mRunning = true; + } + + /** + * Stops this framer from dispatching messages + */ + public void stop() + { + mRunning = false; + } + + /** + * Notification from the symbol processor that a sync pattern was detected, where the final sync dibit is the most + * recently processed dibit. + */ + @Override + public void syncDetected() + { + mSuperFrameDetector.syncDetected(); } /** * Primary method for streaming decoded symbol dibits for message framing. * * @param dibit to process + * @return true if a valid super frame fragment was produced. */ @Override - public void receive(Dibit dibit) + public boolean process(Dibit dibit) { - mSuperFrameDetector.receive(dibit); + return mSuperFrameDetector.process(dibit); } /** - * Primary method for streaming decoded symbol byte arrays. + * Primary method for streaming decoded symbol dibits and soft symbols for message framing. + * + * @param dibit to process + * @param softSymbol for the dibit (radians), for soft decision decoding + * @return true if a valid super frame fragment was produced. + */ + @Override + public boolean process(Dibit dibit, float softSymbol) + { + return mSuperFrameDetector.process(dibit, softSymbol); + } + + /** + * Processes the dibit and detects sync patterns from the dibit stream, for use when processing a stream of dibits + * without a symbol processor (ie replaying a recorded bit stream). + * + * @param dibit to process + */ + public void processWithHardSyncDetect(Dibit dibit) + { + mSuperFrameDetector.processWithHardSyncDetect(dibit); + } + + /** + * Processes the decoded symbol byte array using hard sync detection. * * @param buffer to process into a stream of dibits for processing. */ @@ -129,7 +170,7 @@ public class P25P2MessageFramer implements Listener { for(int x = 0; x <= 3; x++) { - receive(Dibit.parse(value, x)); + processWithHardSyncDetect(Dibit.parse(value, x)); } } } @@ -202,7 +243,8 @@ public class P25P2MessageFramer implements Listener processingChain.start(); - P25P2MessageFramer messageFramer = new P25P2MessageFramer(null); + P25P2MessageFramer messageFramer = new P25P2MessageFramer(); + messageFramer.start(); messageFramer.setScrambleParameters(scrambleParameters); P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor(); messageFramer.setListener(messageProcessor); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SuperFrameDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SuperFrameDetector.java index 3a4c55ca..5acd86ba 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SuperFrameDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SuperFrameDetector.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -19,11 +19,10 @@ package io.github.dsheirer.module.decode.p25.phase2; import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; +import io.github.dsheirer.module.decode.p25.phase2.enumeration.ISCHSequence; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; import io.github.dsheirer.module.decode.p25.phase2.message.SuperFrameFragment; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; @@ -40,14 +39,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * APCO25 Phase 2 super-frame fragment detector uses a pair of sync pattern detectors and a circular dibit buffer to - * detect sync patterns and correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and - * 4x ISCH fragments. The first ISCH fragment consisting of 20-bits from the preceding fragment and 20-bits from + * APCO25 Phase 2 super-frame fragment detector uses sync detection notifications and a circular dibit buffer to + * correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and 4x ISCH fragments. Sync + * detections are provided by the symbol processor's soft sync detector (see syncDetected()) or by checking the dibit + * buffer for the sync pattern (see processWithHardSyncDetect()) when processing a stream of dibits without soft + * symbols. The first ISCH fragment consisting of 20-bits from the preceding fragment and 20-bits from * the current fragment are reassembled into a contiguous ISCH chunk. The layout for the super frame fragment is: * * I-ISCH1 + TIMESLOT1 + I-ISCH2 + TIMESLOT2 + S-SISCH1(sync1) + TIMESLOT3 + S-SISCH2(sync2) + TIMESLOT4 */ -public class P25P2SuperFrameDetector implements Listener, ISyncDetectListener +public class P25P2SuperFrameDetector { private final static Logger mLog = LoggerFactory.getLogger(P25P2SuperFrameDetector.class); @@ -70,8 +71,8 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList private static final int DIBIT_COUNT_MISALIGNED_SYNC = FRAGMENT_DIBIT_LENGTH - 180; /** - * Threshold for broadcasting a sync loss message once per second. Size is the quantity of dibits per second (3000) - * plus the length of one super frame (720). + * Threshold for broadcasting a sync loss message every 3000 dibits (0.5 seconds at 6000 dibits per second) when not + * synchronized. Size is the sync loss broadcast interval (3000) plus the length of one super frame fragment (720). */ private static final int BROADCAST_SYNC_LOSS_DIBIT_COUNT = 3720; @@ -98,14 +99,34 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList */ private static final int UN_SYNCHRONIZED_SYNC_MATCH_THRESHOLD = 4; + /** + * Dibit count between the final dibit of a detected sync pattern and the fragment check, which aligns the detected + * sync pattern with sync pattern 2 in the fragment buffer. This is the 160 dibits between the end of the second + * sync pattern and the end of the fragment plus the extra dibits for the oversized fragment buffer. + */ + private static final int SYNC_DETECTION_DELAY = 160 + FRAGMENT_BUFFER_OVERSIZE; + + /** + * Dibits per second (6000 baud) for calculating message timestamps. + */ + private static final double DIBITS_PER_SECOND = 6000.0; + + /** + * Maximum number of fragments (1.44 seconds) following a fragment with a valid I-ISCH that the ISCH sequence can be + * predicted for. Testing against Phase 2 recordings with injected noise: the predicted sequence matched the + * decoded sequence for all 5,256 fragments with a valid I-ISCH, and increasing the maximum to 48 fragments only + * added predictions at 6 dB in-band C/N. + */ + private static final int MAX_PREDICTION_FRAGMENT_COUNT = 12; + + /** + * Maximum offset (dibits) from a whole number of fragments since the anchor fragment for an ISCH sequence + * prediction, which matches the dibit stuffing/deletion offset range that the fragment checks can correct. + */ + private static final int MAX_PREDICTION_DIBIT_OFFSET = 2; + private ScramblingSequence mScramblingSequence = new ScramblingSequence(); private Listener mMessageListener; - private P25P2SyncDetector mSyncDetector; - /** - * The sync detection delay buffer is sized per the 160 dibits (320 bits) distance between the end of the fragment - * and the end of the second sync pattern (160) plus any extra dibits to account for the oversized fragment buffer. - */ - private DibitDelayBuffer mSyncDetectionDelayBuffer = new DibitDelayBuffer(160 + FRAGMENT_BUFFER_OVERSIZE); /** * The fragment buffer is sized to hold a complete super frame fragment (720 dibits) plus two extra preceding and @@ -113,13 +134,28 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList * stuffed or deleted dibits in the sequence. */ private DibitDelayBuffer mFragmentBuffer = new DibitDelayBuffer(720 + (2 * FRAGMENT_BUFFER_OVERSIZE)); - private int mDibitsProcessed = 0; - private boolean mSynchronized = false; - private ISyncDetectListener mSyncDetectListener; - public P25P2SuperFrameDetector(IPhaseLockedLoop phaseLockedLoop) + /** + * Soft symbol buffer, parallel to the fragment buffer, for soft decision decoding. + */ + private final float[] mSoftSymbolBuffer = new float[720 + (2 * FRAGMENT_BUFFER_OVERSIZE)]; + private int mSoftSymbolPointer = 0; + private boolean mSoftSymbolsAvailable = false; + private int mDibitsProcessed = 0; + private long mDibitCounter = 0; + private long mAnchorDibitCounter = -1; + private ISCHSequence mAnchorIschSequence = ISCHSequence.UNKNOWN; + private int mSyncDetectionCountdown = 0; + private int mDibitsSinceTimestamp = 0; + private long mReferenceTimestamp = 0; + private boolean mSynchronized = false; + private boolean mValidFragment = false; + + /** + * Constructs an instance + */ + public P25P2SuperFrameDetector() { - mSyncDetector = new P25P2SyncDetector(this, phaseLockedLoop); } /** @@ -131,89 +167,154 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList mScramblingSequence.update(scramblingSequence); } - /** - * Sets an external sync detect listener to receive sync detect/loss notifications - */ - public void setSyncDetectListener(ISyncDetectListener listener) - { - mSyncDetectListener = listener; - } - public void setListener(Listener listener) { mMessageListener = listener; } /** - * Sets the sample rate for the phase inversion sync detector + * Sets or updates the current dibit stream time from an incoming sample buffer. + * @param timestamp to use as a reference timestamp. */ - public void setSampleRate(double sampleRate) + public void setTimestamp(long timestamp) { - mSyncDetector.setSampleRate(sampleRate); - } - - public void reset() - { - } - - @Override - public void syncDetected(int bitErrors) - { - checkFragmentSync(bitErrors); - - //Rebroadcast to an external listener - if(mSyncDetectListener != null) - { - mSyncDetectListener.syncDetected(bitErrors); - } - } - - @Override - public void syncLost(int bitsProcessed) - { - //Rebroadcast to an external listener - if(mSyncDetectListener != null) - { - mSyncDetectListener.syncLost(bitsProcessed); - } + mReferenceTimestamp = timestamp; + mDibitsSinceTimestamp = 0; } + /** + * Calculates the timestamp accurate to the currently received dibit. + * @return timestamp in milliseconds. + */ private long getCurrentTimestamp() { - //TODO: implement a dibit counter and timestamp calculator. We should receive a timestamp update with each - //TODO: buffer that arrives. Use the timestamp dibit counter to calculate the exact timestamp of where we're at. + if(mReferenceTimestamp > 0) + { + return mReferenceTimestamp + (long)(1000.0 * mDibitsSinceTimestamp / DIBITS_PER_SECOND); + } + return System.currentTimeMillis(); } - @Override - public void receive(Dibit dibit) + /** + * Notification that a sync pattern was detected where the final sync dibit is the most recently processed dibit. + * When not synchronized, schedules a fragment check once the detected sync pattern aligns with the sync pattern 2 + * location in the fragment buffer. Ignored when synchronized since the fragment is checked at each fragment + * interval. + */ + public void syncDetected() { + if(!mSynchronized && mSyncDetectionCountdown == 0) + { + mSyncDetectionCountdown = SYNC_DETECTION_DELAY; + } + } + + /** + * Processes the dibit and detects sync patterns from the dibits in the fragment buffer, for use when processing a + * stream of dibits without access to the soft symbol sync detection (see syncDetected()). + * @param dibit to process + * @return true if a valid super frame fragment was produced. + */ + public boolean processWithHardSyncDetect(Dibit dibit) + { + boolean valid = process(dibit); + + if(!mSynchronized && mSyncDetectionCountdown == 0) + { + int bitErrorCount = getSyncBitErrorCount(DIBIT_DELAY_BUFFER_INDEX_SYNC_2); + + if(bitErrorCount <= UN_SYNCHRONIZED_SYNC_MATCH_THRESHOLD) + { + mValidFragment = false; + checkFragmentSync(bitErrorCount); + valid |= mValidFragment; + } + } + + return valid; + } + + /** + * Processes the dibit and the soft symbol it was decided from. + * @param dibit to process + * @param softSymbol for the dibit (radians) + * @return true if a valid super frame fragment (ie contains at least one valid MAC message) was produced. + */ + public boolean process(Dibit dibit, float softSymbol) + { + mSoftSymbolBuffer[mSoftSymbolPointer++] = softSymbol; + mSoftSymbolPointer %= mSoftSymbolBuffer.length; + mSoftSymbolsAvailable = true; + return process(dibit); + } + + /** + * Extracts soft symbols from the soft symbol buffer, using the same indexing as the fragment buffer. + * @param start index relative to the oldest soft symbol in the buffer + * @param length number of soft symbols + * @return soft symbols or null if soft symbols are not available. + */ + private float[] getSoftSymbols(int start, int length) + { + if(!mSoftSymbolsAvailable) + { + return null; + } + + float[] softSymbols = new float[length]; + int pointer = (mSoftSymbolPointer + start) % mSoftSymbolBuffer.length; + + for(int x = 0; x < length; x++) + { + softSymbols[x] = mSoftSymbolBuffer[pointer++]; + pointer %= mSoftSymbolBuffer.length; + } + + return softSymbols; + } + + /** + * Processes the dibit. + * @param dibit to process + * @return true if a valid super frame fragment (ie contains at least one valid MAC message) was produced. + */ + public boolean process(Dibit dibit) + { + mValidFragment = false; mDibitsProcessed++; + mDibitsSinceTimestamp++; + mDibitCounter++; mFragmentBuffer.put(dibit); if(mSynchronized) { - mSyncDetectionDelayBuffer.put(dibit); + mSyncDetectionCountdown = 0; if(mDibitsProcessed >= FRAGMENT_DIBIT_LENGTH) { checkFragmentSync(0); } } - else + else if(mSyncDetectionCountdown > 0) { - //Only feed the sync pattern detector if we're not synchronized - Dibit delayed = mSyncDetectionDelayBuffer.getAndPut(dibit); - mSyncDetector.receive(delayed); + mSyncDetectionCountdown--; + + if(mSyncDetectionCountdown == 0) + { + checkFragmentSync(getSyncBitErrorCount(DIBIT_DELAY_BUFFER_INDEX_SYNC_2)); + } } - //Broadcast sync loss message once a second (3000 dibits/6000 bits) when we're not synchronized + //Broadcast sync loss message every 3000 dibits (6000 bits) when we're not synchronized if(mDibitsProcessed > BROADCAST_SYNC_LOSS_DIBIT_COUNT) { mDibitsProcessed -= 3000; broadcastSyncLoss(3000); } + + return mValidFragment; } /** @@ -234,8 +335,12 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList mDibitsProcessed = 0 + dibitOffset; CorrectedBinaryMessage message = mFragmentBuffer.getMessage(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720); message.setCorrectedBitCount(bitErrors); - SuperFrameFragment frameFragment = new SuperFrameFragment(message, getCurrentTimestamp(), mScramblingSequence); + float[] softSymbols = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720); + SuperFrameFragment frameFragment = new SuperFrameFragment(message, softSymbols, getCurrentTimestamp(), + mScramblingSequence); + updateIschSequence(frameFragment, mDibitCounter - FRAGMENT_BUFFER_OVERSIZE + dibitOffset); updateScramblingCode(frameFragment); + mValidFragment = hasValidMacMessage(frameFragment); broadcast(frameFragment); } @@ -269,11 +374,51 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList //Xor the two messages to produce a unified super frame fragment message1.xor(message2); message1.setCorrectedBitCount(bitErrors); //Not even sure what the correct bit error count is here? - SuperFrameFragment frameFragment = new SuperFrameFragment(message1, getCurrentTimestamp(), mScramblingSequence); + //Soft symbols are similarly aligned to sync 1 before dibit 540 and to sync 2 from dibit 540 onward + float[] softSymbols = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + sync1Offset, 720); + float[] softSymbols2 = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + sync2Offset, 720); + + if(softSymbols != null) + { + System.arraycopy(softSymbols2, 540, softSymbols, 540, 180); + } + + SuperFrameFragment frameFragment = new SuperFrameFragment(message1, softSymbols, getCurrentTimestamp(), + mScramblingSequence); + updateIschSequence(frameFragment, mDibitCounter - FRAGMENT_BUFFER_OVERSIZE + sync2Offset); updateScramblingCode(frameFragment); + mValidFragment = hasValidMacMessage(frameFragment); broadcast(frameFragment); } + /** + * Tracks the ISCH sequence (ie fragment 1, 2 or 3 of the superframe) of fragments with a valid I-ISCH as an anchor + * and predicts the ISCH sequence for fragments where neither I-ISCH is valid, from the number of whole fragments + * since the anchor fragment. The ISCH sequence determines the scrambling sequence offset and the order of the + * final two timeslots in the fragment. + * @param fragment to update + * @param fragmentEnd dibit counter value for the final dibit of the fragment + */ + private void updateIschSequence(SuperFrameFragment fragment, long fragmentEnd) + { + if(fragment.hasDecodedIschSequence()) + { + mAnchorDibitCounter = fragmentEnd; + mAnchorIschSequence = fragment.getIschSequence(); + } + else if(mAnchorDibitCounter >= 0) + { + long elapsed = fragmentEnd - mAnchorDibitCounter; + long fragments = Math.round(elapsed / (double)FRAGMENT_DIBIT_LENGTH); + + if(fragments >= 1 && fragments <= MAX_PREDICTION_FRAGMENT_COUNT && + Math.abs(elapsed - fragments * FRAGMENT_DIBIT_LENGTH) <= MAX_PREDICTION_DIBIT_OFFSET) + { + fragment.setPredictedIschSequence(mAnchorIschSequence.advance((int)fragments)); + } + } + } + /** * Updates the scrambling code when we receive a network status broadcast with WACN, SYSTEM and NAC. * @param superFrameFragment @@ -316,6 +461,30 @@ public class P25P2SuperFrameDetector implements Listener, ISyncDetectList } } + /** + * Indicates if the super frame fragment contains at least one valid (ie CRC check passes) MAC message. + * @param superFrameFragment to inspect + * @return true if valid. + */ + private static boolean hasValidMacMessage(SuperFrameFragment superFrameFragment) + { + for(Timeslot timeslot: superFrameFragment.getTimeslots()) + { + if(timeslot instanceof AbstractSignalingTimeslot abstractSignalingTimeslot) + { + for(MacMessage macMessage: abstractSignalingTimeslot.getMacMessages()) + { + if(macMessage.isValid()) + { + return true; + } + } + } + } + + return false; + } + private void broadcastSyncLoss(int dibitsProcessed) { broadcast(new SyncLossMessage(getCurrentTimestamp(), dibitsProcessed * 2, Protocol.APCO25_PHASE2)); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SymbolProcessor.java new file mode 100644 index 00000000..ef990bd8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SymbolProcessor.java @@ -0,0 +1,236 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2; + +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; +import io.github.dsheirer.dsp.c4fm.C4FMSymbolProcessor; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorFactory; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +/** + * P25 Phase 2 HDQPSK implementation of the C4FM symbol processor. Processes the differentially demodulated sample + * stream produced by the differential demodulator, which is in the same radians per sample domain as the C4FM + * decoder's sample stream. + * + * The outbound (FDMA) super frame fragment (720 dibits) contains two S-ISCH sync patterns separated by 180 dibits, so + * the sync interval alternates between 180 and 540 dibits. The 20-symbol sync pattern has an equal quantity of +3 and + * -3 symbols, which supports the default (mean) balance measurement approach of the parent class. + * + * The demodulated sample stream is phase wrapped, so this processor interpolates across the phase wrap (see + * interpolate()). + */ +public class P25P2SymbolProcessor extends C4FMSymbolProcessor +{ + private static final int SYMBOL_RATE = 6000; + //Equivalent outer symbol deviation for the differentially demodulated (+/- 3PI/4 radians per symbol) stream, so that + //the nominal gain scalar is 1.0 (ie 3/8 x 6000 baud) + private static final double OUTER_SYMBOL_DEVIATION = 2250.0; //Hertz + //Sync patterns occur every 180 dibits (S-ISCH 1 to S-ISCH 2) or 540 dibits (S-ISCH 2 to S-ISCH 1) + private static final int SYNC_INTERVAL = 180; + //Allow one missed sync pattern (ie a full 720 dibit fragment) before declaring sync loss + private static final int MAX_SYNC_INTERVAL = 720 + 2; + //Tuning: the following values were tested against Phase 2 recordings with injected noise (valid MAC messages at + //none/12/9/6 dB in-band C/N, two noise seeds). Run to run variation between noise seeds is ~5% at 9 dB, which + //exceeds the variation across the tested values unless noted. + //Once synchronized, sync detections are only allowed within this many symbols of a sync interval boundary. + //Tested 1-5 symbols: no difference. + private static final int SYNC_WINDOW_SYMBOLS = 2; + //Tested 160-1200 symbols and 1-10 frames: no difference with a single symbol equalizer span. + private static final int EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT = 600; + private static final int EQUALIZER_SKIP_TRAINING_FRAME_COUNT = 5; + //Longer equalizer spans reduce valid messages at 9 dB: 3 symbols (-1.4%), 5 symbols (-4.2%), 7 symbols (-6.5%) + private static final int EQUALIZER_SYMBOL_LENGTH = 1; + //Disabling the continuous timing tracking (0) reduced valid messages 13% at 9 dB. Tested 0.001-0.008. + private static final double TIMING_TRACKING_GAIN = 0.002; + //Sync thresholds scaled from the P25 Phase 1 LSM values for the shorter (20 vs 24 symbol) sync pattern. Tested + //detection 45-80 and optimized/equalized 60-100: lower optimized/equalized thresholds (60-65) reduced valid + //messages, 100 reduced sync acquisition at 6 dB, and the remaining values were within the run to run variation. + //The sync waveform validation threshold 0.75 reduced valid messages 36% at 9 dB, and 0.3-0.6 were within the run + //to run variation. + private static final float SYNC_THRESHOLD_DETECTED = 58; + private static final float SYNC_THRESHOLD_OPTIMIZED = 71; + private static final float SYNC_THRESHOLD_EQUALIZED = 71; + private static final double SYNC_WAVEFORM_VALIDATION_THRESHOLD = 0.45; + //Phase wrap period of the demodulated sample stream: 2 * PI radians per symbol, in radians per sample + private float mWrapPeriod = (float)(2.0 * Math.PI); + + /** + * Constructs an instance + * + * @param feedbackDecoder parent decoder + * @param messageFramer to receive symbol decisions (dibits) and sync notifications. + */ + public P25P2SymbolProcessor(FeedbackDecoder feedbackDecoder, C4FMMessageFramer messageFramer) + { + super(feedbackDecoder, messageFramer, P25P2SoftSyncDetectorFactory.getDetector(), + P25P2SoftSyncDetectorFactory.getDetector()); + } + + @Override + public void setSamplesPerSymbol(float samplesPerSymbol) + { + mWrapPeriod = (float)(2.0 * Math.PI / samplesPerSymbol); + super.setSamplesPerSymbol(samplesPerSymbol); + } + + /** + * Interpolates across the phase wrap. The differentially demodulated sample stream is wrapped to +/- PI radians + * per symbol and the outer symbols (+/- 3PI/4) are only PI/4 from the wrap, so consecutive samples near an outer + * symbol can straddle the wrap (ie +3.0 and -3.0) where linear interpolation produces a value near zero. This + * unwraps the second sample relative to the first, interpolates, and wraps the result to +/- PI radians per symbol. + */ + @Override + protected float interpolate(float sample1, float sample2, double mu) + { + float wrap = mWrapPeriod; + float delta = sample2 - sample1; + delta -= wrap * Math.round(delta / wrap); + float interpolated = (float)(sample1 + delta * mu); + return interpolated - wrap * Math.round(interpolated / wrap); + } + + /** + * Calculates the standard deviation of the sample to sample differences across the phase wrap, for the noise + * detection at each sync detection. Each sample difference that straddles the phase wrap (ie +3.0 to -3.0) is + * wrapped to +/- PI radians per symbol. Testing against PCWIN recordings: the unwrapped differences (C4FM + * implementation) exceed the noise threshold for most of the valid sync detections, which prevented + * synchronization on 2 of 6 recordings. + * @param offset to the sample representing the final symbol in the detected sync pattern. + * @return standard deviation. + */ + @Override + public double getStandardDeviation(double offset) + { + StandardDeviation standardDeviation = new StandardDeviation(); + float wrap = mWrapPeriod; + int start = (int)Math.floor(offset - ((mSyncDetector.getDibitLength() - 1) * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + end = Math.min(end, mBuffer.length - 1); + float delta; + + for(int i = start; i < end; i++) + { + delta = mBuffer[i] - mBuffer[i + 1]; + delta -= wrap * Math.round(delta / wrap); + standardDeviation.increment(delta); + } + + return standardDeviation.getResult(); + } + + /** + * Once synchronized with at least two consecutive sync detections, only allows sync detections within a small + * window around each expected sync interval boundary to prevent false sync detections from timeslot payload. + * @param symbolsSinceLastSync count of symbols since the final symbol of the previous sync detection. + * @return true if a sync detection is allowed. + */ + @Override + protected boolean isSyncDetectionAllowed(int symbolsSinceLastSync) + { + if(!mSynchronized || getConsecutiveSyncPeriods() < 2) + { + return true; + } + + int offset = symbolsSinceLastSync % SYNC_INTERVAL; + return Math.min(offset, SYNC_INTERVAL - offset) <= SYNC_WINDOW_SYMBOLS; + } + + /** + * Indicates if the symbol processor is synchronized to the signal (ie sync detected within the max sync interval) + */ + public boolean isSynchronized() + { + return mSynchronized; + } + + @Override + public boolean isDynamicBalanceTrackingRequired() + { + return true; + } + + @Override + public int getMaxMessageDibitLength() + { + return MAX_SYNC_INTERVAL; + } + + @Override + public int getEqualizerTrainingPeriodDurationSymbolCount() + { + return EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT; + } + + @Override + public int getEqualizerSkipTrainingFrameCount() + { + return EQUALIZER_SKIP_TRAINING_FRAME_COUNT; + } + + @Override + public int getEqualizerSymbolLength() + { + return EQUALIZER_SYMBOL_LENGTH; + } + + @Override + public double getTimingTrackingGain() + { + return TIMING_TRACKING_GAIN; + } + + @Override + public int getSymbolRate() + { + return SYMBOL_RATE; + } + + @Override + public double getOuterSymbolDeviation() + { + return OUTER_SYMBOL_DEVIATION; + } + + @Override + public float getSyncThresholdDetected() + { + return SYNC_THRESHOLD_DETECTED; + } + + @Override + public float getSyncThresholdOptimized() + { + return SYNC_THRESHOLD_OPTIMIZED; + } + + @Override + public float getSyncThresholdEqualized() + { + return SYNC_THRESHOLD_EQUALIZED; + } + + @Override + public double getSyncWaveformValidationThreshold() + { + return SYNC_WAVEFORM_VALIDATION_THRESHOLD; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SyncDetector.java deleted file mode 100644 index 20f6a3ea..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2SyncDetector.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2024 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 - * **************************************************************************** - */ -package io.github.dsheirer.module.decode.p25.phase2; - -import io.github.dsheirer.bits.MultiSyncPatternMatcher; -import io.github.dsheirer.bits.SoftSyncDetector; -import io.github.dsheirer.bits.SyncDetector; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.dsp.symbol.FrameSync; -import io.github.dsheirer.dsp.symbol.ISyncDetectListener; -import io.github.dsheirer.sample.Listener; -import org.apache.commons.math3.util.FastMath; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class P25P2SyncDetector implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(P25P2SyncDetector.class); - - /* Determines the threshold for sync pattern soft matching */ - private static final int SYNC_MATCH_THRESHOLD = 4; - - /* Costas Loop phase lock error correction values. A phase lock error of - * 90 degrees requires a correction of 1/4 of the symbol rate (1500Hz). An - * error of 180 degrees requires a correction of 1/2 of the symbol rate */ - public static final double DEFAULT_SAMPLE_RATE = 50000.0; - public static final double DEFAULT_SYMBOL_RATE = 6000; - - public static final double FREQUENCY_PHASE_CORRECTION_90_DEGREES = DEFAULT_SYMBOL_RATE / 4.0; - public static final double FREQUENCY_PHASE_CORRECTION_180_DEGREES = DEFAULT_SYMBOL_RATE / 2.0; - - private MultiSyncPatternMatcher mMatcher; - private SoftSyncDetector mPrimarySyncDetector; - - private PLLPhaseInversionDetector mInversionDetector90CW; - private PLLPhaseInversionDetector mInversionDetector90CCW; - private PLLPhaseInversionDetector mInversionDetector180; - - public P25P2SyncDetector(ISyncDetectListener syncDetectListener, IPhaseLockedLoop phaseLockedLoop) - { - //TODO: since we're only going to feed dibits to find next frame, it makes sense to - //TODO: update the sync lost parameter to 48 bits .... - - //TODO: only enable the phase inversion detectors when we're in a sync-lost state - mMatcher = new MultiSyncPatternMatcher(syncDetectListener, 1440, 40); - mPrimarySyncDetector = new SoftSyncDetector(FrameSync.P25_PHASE2_NORMAL.getSync(), SYNC_MATCH_THRESHOLD, syncDetectListener); - mMatcher.add(mPrimarySyncDetector); - - if(phaseLockedLoop != null) - { -// //Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern -// //detections so that we can apply correction to the phase locked loop -// mInversionDetector90CW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE2_ERROR_90_CW, -// phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES); -// mMatcher.add(mInversionDetector90CW); -// -// mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE2_ERROR_90_CCW, -// phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES); -// mMatcher.add(mInversionDetector90CCW); -// -// mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE2_ERROR_180, -// phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_180_DEGREES); -// mMatcher.add(mInversionDetector180); - } - } - - /** - * Calculates the number of bits that match in the current primary detector - * @return - */ - public int getPrimarySyncMatchErrorCount() - { - return Long.bitCount(mMatcher.getCurrentValue() ^ FrameSync.P25_PHASE2_NORMAL.getSync()); - } - - @Override - public void receive(Dibit dibit) - { - mMatcher.receive(dibit.getBit1(), dibit.getBit2()); - } - - /** - * Updates the incoming sample stream sample rate to allow the PLL phase inversion detectors to - * recalculate their internal phase correction values. - * - * @param sampleRate of the incoming sample stream - */ - public void setSampleRate(double sampleRate) - { -// mInversionDetector180.setSampleRate(sampleRate); -// mInversionDetector90CW.setSampleRate(sampleRate); -// mInversionDetector90CCW.setSampleRate(sampleRate); - } - - /** - * Sync pattern detector to listen for costas loop phase lock errors and apply a phase correction to the costas - * loop so that we don't miss any messages. - * - * When the costas loop locks with a +/- 90 degree or 180 degree phase error, the slicer will incorrectly apply - * the symbol pattern rotated left or right by the phase error. However, we can detect these rotated sync patterns - * and apply immediate phase correction so that message processing can continue. - */ - public class PLLPhaseInversionDetector extends SyncDetector - { - private FrameSync mFrameSync; - private IPhaseLockedLoop mPhaseLockedLoop; - private double mSampleRate; - private double mFrequencyCorrection; - private double mPllCorrection; - - /** - * Constructs the PLL phase inversion detector. - * - * @param frameSync pattern to monitor for detecting phase inversion errors - * @param phaseLockedLoop to receive phase correction values - * @param sampleRate of the incoming sample stream - * @param frequencyCorrection to apply to the PLL. Examples: - * QPSK +/-90 degree correction: +/-SYMBOL RATE / 4.0 - * QPSK 180 degree correction: SYMBOL RATE / 2.0 - */ - public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double sampleRate, - double frequencyCorrection) - { - super(frameSync.getSync()); - mFrameSync = frameSync; - mPhaseLockedLoop = phaseLockedLoop; - mFrequencyCorrection = frequencyCorrection; - setSampleRate(sampleRate); - - setListener(new ISyncDetectListener() - { - @Override - public void syncDetected(int bitErrors) - { - mPhaseLockedLoop.correctInversion(mPllCorrection); - } - - @Override - public void syncLost(int bitsProcessed) - { - //no-op - } - }); - } - - /** - * Sets or adjusts the sample rate so that the phase inversion correction value can be recalculated. - * @param sampleRate - */ - public void setSampleRate(double sampleRate) - { - mSampleRate = sampleRate; - mPllCorrection = 2.0 * FastMath.PI * mFrequencyCorrection / mSampleRate; - } - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequence.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequence.java index 57ed8c18..d11ae617 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequence.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequence.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -57,6 +57,22 @@ public enum ISCHSequence return mTimeslotOffset; } + /** + * Sequence of the fragment that follows this fragment by the specified number of fragments in the repeating + * sequence of 3 fragments per superframe. + * @param fragmentCount number of fragments to advance + * @return advanced sequence or UNKNOWN if this is not a fragment (1-3) sequence. + */ + public ISCHSequence advance(int fragmentCount) + { + if(this == ISCH_1 || this == ISCH_2 || this == ISCH_3) + { + return fromValue((getValue() + fragmentCount) % 3); + } + + return UNKNOWN; + } + /** * Indicates if this sequence is a final fragment sequence */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java index 6764206b..c6b6b188 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -21,7 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase2.message; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.edac.ReedSolomon_44_16_29_P25; +import io.github.dsheirer.edac.ReedSolomonErasureDecoder; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice4Timeslot; @@ -42,6 +42,10 @@ import org.slf4j.LoggerFactory; public class EncryptionSynchronizationSequenceProcessor { private final static Logger mLog = LoggerFactory.getLogger(EncryptionSynchronizationSequenceProcessor.class); + /** + * RS(44,16,29) shortened from RS(63,35,29). The decoder has no mutable state. + */ + private static final ReedSolomonErasureDecoder REED_SOLOMON = ReedSolomonErasureDecoder.p25_63_35_29(); private BinaryMessage mESSA; private BinaryMessage mESSB1; @@ -114,70 +118,51 @@ public class EncryptionSynchronizationSequenceProcessor public EncryptionSynchronizationSequence getSequence() { //We need a minimum of ESS-A and at least one of the ESS-B sequences before we can decode. The Reed Solomon - //code can correct up to 14 symbol errors across the 16 info symbols provided by ESS-B and the 28 parity symbols - //provided by ESS-A + //code can correct v symbol errors and e erasures, where 2v + e <= 28, across the 16 info symbols provided by + //ESS-B and the 28 parity symbols provided by ESS-A. The symbols for any missing ESS-B fragments are first + //decoded as zero valued symbols, which are correct for unencrypted calls (ie zero key ID and message + //indicator), and then as erasures, which supports encrypted calls. if(mESSA != null && (mESSB1 != null || mESSB2 != null || mESSB3 != null || mESSB4 != null)) { //We have to reverse the order of the information hexbits and the RS parity hexbits in the array for //the RS algorithm. int[] input = new int[63]; - - int inputPointer = 0; + int[] erasures = new int[12]; + int erasureCount = 0; for(int x = 27; x >= 0; x--) { - input[inputPointer++] = mESSA.getInt(x * 6, x * 6 + 5); + input[27 - x] = mESSA.getInt(x * 6, x * 6 + 5); } - if(mESSB4 != null) - { - for(int x = 3; x >= 0; x--) - { - input[inputPointer++] = mESSB4.getInt(x * 6, x * 6 + 5); - } - } - else - { - inputPointer += 4; - } + BinaryMessage[] essB = {mESSB4, mESSB3, mESSB2, mESSB1}; - if(mESSB3 != null) + for(int fragment = 0; fragment < essB.length; fragment++) { - for(int x = 3; x >= 0; x--) - { - input[inputPointer++] = mESSB3.getInt(x * 6, x * 6 + 5); - } - } - else - { - inputPointer += 4; - } + int inputPointer = 28 + (fragment * 4); - if(mESSB2 != null) - { for(int x = 3; x >= 0; x--) { - input[inputPointer++] = mESSB2.getInt(x * 6, x * 6 + 5); - } - } - else - { - inputPointer += 4; - } + if(essB[fragment] != null) + { + input[inputPointer] = essB[fragment].getInt(x * 6, x * 6 + 5); + } + else + { + erasures[erasureCount++] = inputPointer; + } - if(mESSB1 != null) - { - for(int x = 3; x >= 0; x--) - { - input[inputPointer++] = mESSB1.getInt(x * 6, x * 6 + 5); + inputPointer++; } } int[] output = new int[63]; + boolean irrecoverableErrors = decode(input, output, erasures, 0); - ReedSolomon_44_16_29_P25 rs = new ReedSolomon_44_16_29_P25(); - - boolean irrecoverableErrors = rs.decode(input, output); + if(irrecoverableErrors && erasureCount > 0) + { + irrecoverableErrors = decode(input, output, erasures, erasureCount); + } if(!irrecoverableErrors) { @@ -188,11 +173,7 @@ public class EncryptionSynchronizationSequenceProcessor for(int x = 43; x >= 28; x--) { - if(output[x] != -1) - { - message.load(pointer, 6, output[x]); - } - + message.load(pointer, 6, output[x]); pointer += 6; } @@ -203,6 +184,33 @@ public class EncryptionSynchronizationSequenceProcessor return null; } + /** + * Decodes the RS(44,16,29) codeword. + * @param input codeword + * @param output for the corrected codeword + * @param erasures positions of the erased symbols + * @param erasureCount number of erasure positions to use + * @return true if the codeword has irrecoverable errors or a correction is located in a shortened symbol. + */ + private static boolean decode(int[] input, int[] output, int[] erasures, int erasureCount) + { + if(REED_SOLOMON.decode(input, output, erasures, erasureCount)) + { + return true; + } + + //The shortened symbols are known to be zero, so a correction in a shortened symbol is a decoding failure + for(int x = 44; x < output.length; x++) + { + if(output[x] != 0) + { + return true; + } + } + + return false; + } + /** * ESS-B fragment 1 */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/SuperFrameFragment.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/SuperFrameFragment.java index d6c30b16..0d9f2330 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/SuperFrameFragment.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/SuperFrameFragment.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -23,6 +23,7 @@ import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.p25.phase2.enumeration.ISCHSequence; import io.github.dsheirer.module.decode.p25.phase2.message.isch.IISCH; import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder; import io.github.dsheirer.module.decode.p25.phase2.message.isch.SISCH; @@ -31,6 +32,7 @@ import io.github.dsheirer.module.decode.p25.phase2.timeslot.Timeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.TimeslotFactory; import io.github.dsheirer.protocol.Protocol; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -56,6 +58,7 @@ public class SuperFrameFragment implements IMessage private static final ISCHDecoder ISCH_DECODER = new ISCHDecoder(); private long mTimestamp; private CorrectedBinaryMessage mMessage; + private float[] mSoftSymbols; private IISCH mIISCH1; private IISCH mIISCH2; private SISCH mSISCH1; @@ -66,6 +69,7 @@ public class SuperFrameFragment implements IMessage private Timeslot mTimeslotD; private List mTimeslots; private ScramblingSequence mScramblingSequence; + private ISCHSequence mPredictedIschSequence = ISCHSequence.UNKNOWN; /** @@ -75,8 +79,22 @@ public class SuperFrameFragment implements IMessage * @param timestamp of the final bit of this fragment */ public SuperFrameFragment(CorrectedBinaryMessage message, long timestamp, ScramblingSequence scramblingSequence) + { + this(message, null, timestamp, scramblingSequence); + } + + /** + * Constructs a fragment from the message and the soft symbols with the specified timestamp. + * + * @param message containing 1440-bit super-frame fragment (ie 1/3 of a superframe) + * @param softSymbols (radians) for each of the 720 dibits in the message, or null, for soft decision decoding + * @param timestamp of the final bit of this fragment + */ + public SuperFrameFragment(CorrectedBinaryMessage message, float[] softSymbols, long timestamp, + ScramblingSequence scramblingSequence) { mMessage = message; + mSoftSymbols = softSymbols; mTimestamp = timestamp; mScramblingSequence = scramblingSequence; } @@ -118,7 +136,6 @@ public class SuperFrameFragment implements IMessage CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage() .getSubMessage(CHANNEL_A_ISCH_START, TIMESLOT_A_START), P25P2Message.TIMESLOT_1); mIISCH1 = new IISCH(message, 0, 1, getTimestamp()); - mIISCH1.setValid(message.getCorrectedBitCount() < 3); } return mIISCH1; @@ -134,7 +151,6 @@ public class SuperFrameFragment implements IMessage CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage() .getSubMessage(CHANNEL_B_ISCH_START, TIMESLOT_B_START), P25P2Message.TIMESLOT_2); mIISCH2 = new IISCH(message, 0, 2, getTimestamp()); - mIISCH2.setValid(message.getCorrectedBitCount() < 3); } return mIISCH2; @@ -309,7 +325,46 @@ public class SuperFrameFragment implements IMessage { CorrectedBinaryMessage message = getMessage().getSubMessage(start, end); BinaryMessage timeslotSequence = mScramblingSequence.getTimeslotSequence(getTimeslotOffset() + index); - return TimeslotFactory.getTimeslot(message, timeslotSequence, timeslot, getTimestamp()); + float[] softSymbols = mSoftSymbols != null ? Arrays.copyOfRange(mSoftSymbols, start / 2, end / 2) : null; + return TimeslotFactory.getTimeslot(message, softSymbols, timeslotSequence, timeslot, getTimestamp()); + } + + /** + * Sets the ISCH sequence predicted from the sequence of a preceding fragment, for use when neither of the I-ISCH + * in this fragment are valid. This must be set before accessing the timeslots. + * @param predicted sequence + */ + public void setPredictedIschSequence(ISCHSequence predicted) + { + mPredictedIschSequence = predicted; + } + + /** + * Indicates if the ISCH sequence for this fragment was decoded from a valid I-ISCH. + */ + public boolean hasDecodedIschSequence() + { + return getIISCH1().isValid() || getIISCH2().isValid(); + } + + /** + * ISCH sequence (ie fragment 1, 2 or 3 of the superframe) decoded from a valid I-ISCH, or the predicted sequence + * when neither of the I-ISCH are valid. + * @return sequence or UNKNOWN + */ + public ISCHSequence getIschSequence() + { + if(getIISCH1().isValid()) + { + return getIISCH1().getIschSequence(); + } + + if(getIISCH2().isValid()) + { + return getIISCH2().getIschSequence(); + } + + return mPredictedIschSequence; } /** @@ -317,37 +372,17 @@ public class SuperFrameFragment implements IMessage */ private int getTimeslotOffset() { - if(getIISCH1().isValid()) - { - return getIISCH1().getIschSequence().getTimeslotOffset(); - } - - if(getIISCH2().isValid()) - { - return getIISCH2().getIschSequence().getTimeslotOffset(); - } - - return 0; + return getIschSequence().getTimeslotOffset(); } /** * Indicates if this fragment is the final fragment (3 of 3) for a superframe which indicates that the final two - * timeslots are inverted. + * timeslots are inverted. When the sequence can't be determined, the higher probability is that it's not the + * final fragment. */ private boolean isFinalFragment() { - if(getIISCH1().isValid()) - { - return getIISCH1().getIschSequence().isFinalFragment(); - } - - if(getIISCH2().isValid()) - { - return getIISCH2().getIschSequence().isFinalFragment(); - } - - //Can't determine, so higher probability is that it's not the final fragment - return false; + return getIschSequence().isFinalFragment(); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoder.java index a1015517..1a1644cf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoder.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -41,6 +41,7 @@ public class ISCHDecoder private static final long[] GENERATOR = {0x8816CE36D7l, 0x201DFD4F64l, 0x100F4B1758l, 0x0C00DED18El, 0x020807F7FFl, 0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l}; private static final long CODE_WORD_OFFSET = 0x184229d461l; + private static final int MAX_CORRECTABLE_BIT_ERRORS = 7; /** * Constructs the ISCH-I parsing class @@ -93,6 +94,46 @@ public class ISCHDecoder return codeword; } + /** + * Decodes the codeword to the nearest (minimum Hamming distance) valid codeword in the map. The (40,9,16) code + * has a minimum distance of 16, so it corrects up to 7 bit errors. + * @param codeword received + * @param codewordMap of valid codewords to messages + * @return decoded message with the corrected bit count, or null if no valid codeword is within 7 bit errors. + */ + private static CorrectedBinaryMessage decode(long codeword, Map codewordMap) + { + BinaryMessage exact = codewordMap.get(codeword); + + if(exact != null) + { + return new CorrectedBinaryMessage(exact); + } + + int smallestErrorCount = MAX_CORRECTABLE_BIT_ERRORS + 1; + BinaryMessage closest = null; + + for(Map.Entry entry: codewordMap.entrySet()) + { + int errorCount = Long.bitCount(codeword ^ entry.getKey()); + + if(errorCount < smallestErrorCount) + { + smallestErrorCount = errorCount; + closest = entry.getValue(); + } + } + + if(closest != null) + { + CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(closest); + decoded.setCorrectedBitCount(smallestErrorCount); + return decoded; + } + + return null; + } + /** * Decodes the 40-bit message codeword into an error-corrected 9-bit message. The expected timeslot allows us to * only use the codewords that are correct for the expected timeslot. @@ -105,64 +146,20 @@ public class ISCHDecoder if(expectedTimeslot == 1) { - if(CODEWORD_MAP_TS1.containsKey(codeword)) + CorrectedBinaryMessage decoded = decode(codeword, CODEWORD_MAP_TS1); + + if(decoded != null) { - return new CorrectedBinaryMessage(CODEWORD_MAP_TS1.get(codeword)); - } - else - { - int smallestErrorCount = 8; - long closestCodeword = 0; - - for(long validCodeword: CODEWORD_MAP_TS1.keySet()) - { - long mask = codeword & validCodeword; - int errorCount = Long.bitCount(mask); - - if(errorCount < smallestErrorCount) - { - smallestErrorCount = errorCount; - closestCodeword = validCodeword; - } - } - - if(closestCodeword != 0 && smallestErrorCount <= 7) //Max correctable bit errors of 7 - { - CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(CODEWORD_MAP_TS1.get(closestCodeword)); - decoded.setCorrectedBitCount(smallestErrorCount); - return decoded; - } + return decoded; } } else if(expectedTimeslot == 2) { - if(CODEWORD_MAP_TS2.containsKey(codeword)) + CorrectedBinaryMessage decoded = decode(codeword, CODEWORD_MAP_TS2); + + if(decoded != null) { - return new CorrectedBinaryMessage(CODEWORD_MAP_TS2.get(codeword)); - } - else - { - int smallestErrorCount = 16; - long closestCodeword = 0; - - for(long validCodeword: CODEWORD_MAP_TS2.keySet()) - { - long mask = codeword & validCodeword; - int errorCount = Long.bitCount(mask); - - if(errorCount < smallestErrorCount) - { - smallestErrorCount = errorCount; - closestCodeword = validCodeword; - } - } - - if(closestCodeword != 0 && smallestErrorCount <= 7) //Max correctable bit errors of 7 - { - CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(CODEWORD_MAP_TS2.get(closestCodeword)); - decoded.setCorrectedBitCount(smallestErrorCount); - return decoded; - } + return decoded; } } else diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java index c14dc1a9..032b1874 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -162,6 +162,30 @@ public class MacMessageFactory private final static Logger mLog = LoggerFactory.getLogger(MacMessageFactory.class); + /** + * Indicates if the message passes the CRC check for the data unit (FACCH, SACCH or LCCH). + * @param dataUnitID that identifies the message type + * @param message to check + * @return true if the CRC check passes or the data unit has no CRC. + */ + public static boolean passesCRC(DataUnitID dataUnitID, CorrectedBinaryMessage message) + { + if(dataUnitID.isFACCH()) + { + return CRCP25.crc12_FACCH(message); + } + else if(dataUnitID.isLCCH()) + { + return CRCP25.crc16_LCCH(message); + } + else if(dataUnitID.isSACCH()) + { + return CRCP25.crc12_SACCH(message); + } + + return true; + } + /** * Creates a set of MAC messages * @param timeslot for the messages @@ -176,26 +200,13 @@ public class MacMessageFactory { List messages = new ArrayList<>(); - boolean passesCRC = true; + boolean passesCRC = passesCRC(dataUnitID, message); int nac = 0; - if(dataUnitID.isFACCH()) + if(passesCRC && dataUnitID.isLCCH()) { - passesCRC = CRCP25.crc12_FACCH(message); - } - else if(dataUnitID.isLCCH()) - { - passesCRC = CRCP25.crc16_LCCH(message); - - if(passesCRC) - { - nac = MacStructure.getLcchNac(message); - } - } - else if(dataUnitID.isSACCH()) - { - passesCRC = CRCP25.crc12_SACCH(message); + nac = MacStructure.getLcchNac(message); } if(!passesCRC) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetector.java new file mode 100644 index 00000000..83f3f377 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetector.java @@ -0,0 +1,79 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import io.github.dsheirer.dsp.c4fm.DibitSyncDetector; +import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector; +import java.util.Arrays; + +/** + * P25 Phase 2 soft sync detector. Correlates the soft symbols (radians) against the 20-symbol S-ISCH sync pattern. + */ +public abstract class P25P2SoftSyncDetector extends DibitSyncDetector implements ISoftC4FMSyncDetector +{ + /** + * P25 Phase 2 S-ISCH sync pattern: +333 -3 +33 -3 +3333 -333 +3 -33333 + */ + public static final long SYNC_PATTERN = 0x575D57F7FFL; + protected static final int SYNC_DIBIT_LENGTH = 20; + protected float[] mSymbols = new float[SYNC_DIBIT_LENGTH * 2]; + protected int mSymbolPointer = 0; + + /** + * Constructs an instance + */ + public P25P2SoftSyncDetector() + { + super(SYNC_PATTERN, SYNC_DIBIT_LENGTH); + } + + /** + * Resets this sync detector and flushes any stored soft symbols from the buffer. + */ + public void reset() + { + Arrays.fill(mSymbols, 0.0f); + mSymbolPointer = 0; + } + + /** + * Worker method to be implemented by the subclass implementation. + * + * @return sync correlation score. + */ + protected abstract float calculate(); + + /** + * Processes the demodulated soft symbol value and returns a correlation value against the preceding 20 soft + * symbols that include this recent soft symbol. + * + * @param softSymbol to process that represents the soft demodulated symbol value in phase angle/radians. + * @return correlation score. + */ + @Override + public float process(float softSymbol) + { + mSymbols[mSymbolPointer] = softSymbol; + mSymbols[mSymbolPointer + SYNC_DIBIT_LENGTH] = softSymbol; + mSymbolPointer++; + mSymbolPointer %= SYNC_DIBIT_LENGTH; + return calculate(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorFactory.java new file mode 100644 index 00000000..203ebc40 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorFactory.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import io.github.dsheirer.vector.calibrate.CalibrationManager; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; + +/** + * Factory for creating P25 Phase 2 Soft Sync Detectors + */ +public class P25P2SoftSyncDetectorFactory +{ + /** + * Creates the implementation using calibration data to select the optimal implementation from scalar and vector + * options. + * @return sync detector instance + */ + public static P25P2SoftSyncDetector getDetector() + { + Implementation implementation = CalibrationManager.getInstance() + .getImplementation(CalibrationType.P25P2_SOFT_SYNC_DETECTOR); + + return switch(implementation) + { + case VECTOR_SIMD_64 -> new P25P2SoftSyncDetectorVector64(); + case VECTOR_SIMD_128 -> new P25P2SoftSyncDetectorVector128(); + case VECTOR_SIMD_256 -> new P25P2SoftSyncDetectorVector256(); + case VECTOR_SIMD_512 -> new P25P2SoftSyncDetectorVector512(); + default -> new P25P2SoftSyncDetectorScalar(); + }; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorScalar.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorScalar.java new file mode 100644 index 00000000..4dd50713 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorScalar.java @@ -0,0 +1,45 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +/** + * Scalar implementation of P25 Phase 2 Soft Sync Detector. + */ +public class P25P2SoftSyncDetectorScalar extends P25P2SoftSyncDetector +{ + /** + * Calculates the correlation of the preceding 20 soft symbols against the sync pattern. + * + * @return correlation score. + */ + @Override + public float calculate() + { + float[] patternSymbols = getPatternSymbols(); + float score = 0; + + for(int x = 0; x < SYNC_DIBIT_LENGTH; x++) + { + score += patternSymbols[x] * mSymbols[mSymbolPointer + x]; + } + + return score; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector128.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector128.java new file mode 100644 index 00000000..8359913d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector128.java @@ -0,0 +1,51 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/** + * SIMD Vector 128 implementation of P25 Phase 2 Soft Sync Detector. + */ +public class P25P2SoftSyncDetectorVector128 extends P25P2SoftSyncDetector +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_128; + + /** + * Calculates the correlation of the preceding 20 soft symbols against the sync pattern. + * + * @return correlation score. + */ + @Override + public float calculate() + { + FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); + + for(int x = 0; x < SYNC_DIBIT_LENGTH; x += VECTOR_SPECIES.length()) + { + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) + .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); + } + + return accumulator.reduceLanes(VectorOperators.ADD); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector256.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector256.java new file mode 100644 index 00000000..f97b408f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector256.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/** + * SIMD Vector 256 implementation of P25 Phase 2 Soft Sync Detector. + */ +public class P25P2SoftSyncDetectorVector256 extends P25P2SoftSyncDetector +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_256; + //Final pattern symbols, zero padded to the vector length, for the final (overlapping) vector of symbols + private final float[] mTailPatternSymbols = new float[8]; + + /** + * Constructs an instance + */ + public P25P2SoftSyncDetectorVector256() + { + System.arraycopy(getPatternSymbols(), 16, mTailPatternSymbols, 4, 4); + } + + /** + * Calculates the correlation of the preceding 20 soft symbols against the sync pattern. + * + * @return correlation score. + */ + @Override + public float calculate() + { + //There are 20 symbols that we load into 3x vectors of 8. The third vector overlaps the second vector by 4 + //symbols (12-15), so it is multiplied against the final 4 pattern symbols (16-19) zero padded in the first 4 + //lanes. Multiplying against zero is cheaper than a masked load. + FloatVector accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer) + .mul(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0)); + accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8) + .fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 8), accumulator); + accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 12) + .fma(FloatVector.fromArray(VECTOR_SPECIES, mTailPatternSymbols, 0), accumulator); + return accumulator.reduceLanes(VectorOperators.ADD); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector512.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector512.java new file mode 100644 index 00000000..ab7d410d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector512.java @@ -0,0 +1,60 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/** + * SIMD Vector 512 implementation of P25 Phase 2 Soft Sync Detector. + */ +public class P25P2SoftSyncDetectorVector512 extends P25P2SoftSyncDetector +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; + //Final pattern symbols, zero padded to the vector length, for the final (overlapping) vector of symbols + private final float[] mTailPatternSymbols = new float[16]; + + /** + * Constructs an instance + */ + public P25P2SoftSyncDetectorVector512() + { + System.arraycopy(getPatternSymbols(), 16, mTailPatternSymbols, 12, 4); + } + + /** + * Calculates the correlation of the preceding 20 soft symbols against the sync pattern. + * + * @return correlation score. + */ + @Override + public float calculate() + { + //There are 20 symbols that we load into 2x vectors of 16. The second vector overlaps the first vector by 12 + //symbols (4-15), so it is multiplied against the final 4 pattern symbols (16-19) zero padded in the first 12 + //lanes. Multiplying against zero is cheaper than a masked load. + FloatVector accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer) + .mul(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0)); + accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 4) + .fma(FloatVector.fromArray(VECTOR_SPECIES, mTailPatternSymbols, 0), accumulator); + return accumulator.reduceLanes(VectorOperators.ADD); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector64.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector64.java new file mode 100644 index 00000000..f9794647 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorVector64.java @@ -0,0 +1,51 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.sync; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/** + * SIMD Vector 64 implementation of P25 Phase 2 Soft Sync Detector. + */ +public class P25P2SoftSyncDetectorVector64 extends P25P2SoftSyncDetector +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_64; + + /** + * Calculates the correlation of the preceding 20 soft symbols against the sync pattern. + * + * @return correlation score. + */ + @Override + public float calculate() + { + FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); + + for(int x = 0; x < SYNC_DIBIT_LENGTH; x += VECTOR_SPECIES.length()) + { + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) + .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); + } + + return accumulator.reduceLanes(VectorOperators.ADD); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/AbstractSignalingTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/AbstractSignalingTimeslot.java index 6105cdb6..85920e7e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/AbstractSignalingTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/AbstractSignalingTimeslot.java @@ -1,7 +1,7 @@ /* * * * ****************************************************************************** - * * Copyright (C) 2014-2019 Dennis Sheirer + * * 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 @@ -24,10 +24,15 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot; 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.edac.ReedSolomonErasureDecoder; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; - +import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,6 +41,38 @@ import java.util.List; */ public abstract class AbstractSignalingTimeslot extends Timeslot { + /** + * RS(63,35,29) errors-and-erasures decoder shared by the signaling timeslots. The decoder has no mutable state. + */ + private static final ReedSolomonErasureDecoder REED_SOLOMON = ReedSolomonErasureDecoder.p25_63_35_29(); + /** + * Index of the first message (information) symbol in the RS(63,35,29) codeword. Lower indices are parity symbols. + */ + private static final int FIRST_MESSAGE_INDEX = 28; + /** + * Soft decision decoding: number of additional (least reliable) received symbols erased for each decoding attempt + * after the punctured parity symbols-only attempt fails. Each step of 2 erasures reduces the error correction + * capacity by one symbol error. + */ + private static final int SOFT_ERASURE_STEP = 2; + /** + * Maximum total (punctured plus soft) erasures for soft decision decoding attempts. Each attempt is a chance for a + * miscorrection to pass the CRC check, and the miscorrection probability increases as the erasures consume the error + * correction capacity. Testing against Phase 2 recordings with injected noise (valid MAC messages at 9 dB in-band + * C/N) and against random (noise) timeslots (SACCH CRC-valid messages per timeslot): + * punctured erasures only: 3125, 0/200k + * 14: 3321, 0/200k + * 20: 3391, 1/200k + * 24: 8/200k + * 28: 3405, 91/200k + * FACCH (9 shortened symbols) produced no CRC-valid messages from random timeslots at any setting. + */ + private static final int SOFT_ERASURE_MAX = 20; + private static final float HALF_PI = (float)(Math.PI / 2.0); + private static final float PI = (float)Math.PI; + private static final float TWO_PI = (float)(2.0 * Math.PI); + private float[] mSoftSymbols; + /** * Constructs a scrambled signaling timeslot * @@ -75,4 +112,224 @@ public abstract class AbstractSignalingTimeslot extends Timeslot * Access Encoded MAC Information (EMI) message(s) carried by the signalling timeslot */ public abstract List getMacMessages(); + + /** + * Sets the soft symbols for this timeslot to enable soft decision decoding. + * @param softSymbols (radians) for each of the 160 dibits in the timeslot. + */ + public void setSoftSymbols(float[] softSymbols) + { + mSoftSymbols = softSymbols; + } + + /** + * Indicates if soft symbols are available for soft decision decoding. + */ + protected boolean hasSoftSymbols() + { + return mSoftSymbols != null; + } + + /** + * Reliability of the dibit: distance (radians) of the soft symbol from the nearest symbol decision boundary + * (0, +/- PI/2 and the +/- PI phase wrap). + * @param dibitIndex in the timeslot + * @return reliability + */ + private float getDibitReliability(int dibitIndex) + { + float symbol = mSoftSymbols[dibitIndex]; + symbol -= TWO_PI * Math.round(symbol / TWO_PI); + float magnitude = Math.abs(symbol); + return Math.min(Math.min(magnitude, Math.abs(magnitude - HALF_PI)), PI - magnitude); + } + + /** + * Loads the hexbit (6-bit symbol) field value into the codeword and the reliability (weakest of the dibits that + * carry the symbol) into the reliabilities array, when soft symbols are available. + * @param codeword to load + * @param reliabilities to load, or null + * @param index of the symbol in the codeword + * @param field for the symbol + */ + protected void load(int[] codeword, float[] reliabilities, int index, IntField field) + { + codeword[index] = getInt(field); + + if(reliabilities != null) + { + float reliability = Float.MAX_VALUE; + + for(int bit = field.start(); bit <= field.end(); bit += 2) + { + reliability = Math.min(reliability, getDibitReliability(bit / 2)); + } + + reliabilities[index] = reliability; + } + } + + /** + * Loads the hexbit (6-bit symbol) fragmented field value into the codeword and the reliability into the + * reliabilities array, when soft symbols are available. + * @param codeword to load + * @param reliabilities to load, or null + * @param index of the symbol in the codeword + * @param field for the symbol + */ + protected void load(int[] codeword, float[] reliabilities, int index, FragmentedIntField field) + { + codeword[index] = getInt(field); + + if(reliabilities != null) + { + float reliability = Float.MAX_VALUE; + + for(int bit : field.indices()) + { + reliability = Math.min(reliability, getDibitReliability(bit / 2)); + } + + reliabilities[index] = reliability; + } + } + + /** + * Creates a reliabilities array for the RS codeword when soft symbols are available. + * @return reliabilities array or null. + */ + protected float[] createReliabilities() + { + return hasSoftSymbols() ? new float[63] : null; + } + + /** + * Decodes the RS(63,35,29) codeword and creates the MAC message(s). The punctured parity symbols are decoded as + * erasures. When soft symbol reliabilities are available and the decoded message fails the RS decode or the CRC + * check, this applies generalized minimum distance decoding: repeats the decode with an increasing number of the + * least reliable received symbols erased, until a decoded message passes the CRC check. + * + * @param codeword received symbols where index 0 to (puncturedCount - 1) are punctured parity symbols and the + * indices above the final message index are shortened (zero) symbols. + * @param reliabilities for each received symbol, or null. + * @param puncturedCount number of punctured parity symbols + * @param finalMessageIndex index of the final (first transmitted) message symbol + * @param maxOctetIndex for the MAC message factory + * @return MAC messages + */ + protected List decode(int[] codeword, float[] reliabilities, int puncturedCount, int finalMessageIndex, + int maxOctetIndex) + { + int[] erasures = new int[REED_SOLOMON.getParityCount()]; + + for(int x = 0; x < puncturedCount; x++) + { + erasures[x] = x; + } + + int[] output = new int[63]; + boolean failed = decode(codeword, output, erasures, puncturedCount, finalMessageIndex); + CorrectedBinaryMessage message = getMessage(codeword, output, puncturedCount, finalMessageIndex); + + if(!failed && MacMessageFactory.passesCRC(getDataUnitID(), message)) + { + return MacMessageFactory.create(getTimeslot(), getDataUnitID(), message, getTimestamp(), maxOctetIndex); + } + + if(reliabilities != null) + { + //Received symbol indices sorted by ascending reliability + List indices = new ArrayList<>(); + + for(int x = puncturedCount; x <= finalMessageIndex; x++) + { + indices.add(x); + } + + indices.sort((a, b) -> Float.compare(reliabilities[a], reliabilities[b])); + int[] softOutput = new int[63]; + + for(int erasureCount = puncturedCount + SOFT_ERASURE_STEP; erasureCount <= SOFT_ERASURE_MAX; + erasureCount += SOFT_ERASURE_STEP) + { + for(int x = puncturedCount; x < erasureCount; x++) + { + erasures[x] = indices.get(x - puncturedCount); + } + + if(!decode(codeword, softOutput, erasures, erasureCount, finalMessageIndex)) + { + CorrectedBinaryMessage softMessage = getMessage(codeword, softOutput, puncturedCount, + finalMessageIndex); + + if(MacMessageFactory.passesCRC(getDataUnitID(), softMessage)) + { + return MacMessageFactory.create(getTimeslot(), getDataUnitID(), softMessage, getTimestamp(), + maxOctetIndex); + } + } + } + } + + if(failed) + { + MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), message, getTimestamp(), + new MacStructureFailedRS(message, 0)); + macMessage.setValid(false); + List macMessages = new ArrayList<>(); + macMessages.add(macMessage); + return macMessages; + } + + return MacMessageFactory.create(getTimeslot(), getDataUnitID(), message, getTimestamp(), maxOctetIndex); + } + + /** + * Decodes the codeword with the erasures. + * @return true if the codeword has irrecoverable errors or a correction is located in a shortened symbol. + */ + private static boolean decode(int[] codeword, int[] output, int[] erasures, int erasureCount, int finalMessageIndex) + { + if(REED_SOLOMON.decode(codeword, output, erasures, erasureCount)) + { + return true; + } + + //Shortened symbols are known to be zero, so a correction in a shortened symbol is a decoding failure + for(int x = finalMessageIndex + 1; x < output.length; x++) + { + if(output[x] != 0) + { + return true; + } + } + + return false; + } + + /** + * Creates the MAC message from the decoded message symbols with the count of corrected bits in the received symbols. + */ + private static CorrectedBinaryMessage getMessage(int[] codeword, int[] output, int puncturedCount, + int finalMessageIndex) + { + CorrectedBinaryMessage message = new CorrectedBinaryMessage((finalMessageIndex - FIRST_MESSAGE_INDEX + 1) * 6); + int pointer = 0; + + for(int x = finalMessageIndex; x >= FIRST_MESSAGE_INDEX; x--) + { + message.load(pointer, 6, output[x]); + pointer += 6; + } + + for(int x = puncturedCount; x <= finalMessageIndex; x++) + { + if(output[x] != codeword[x]) + { + message.incrementCorrectedBitCount(Integer.bitCount(output[x] ^ codeword[x])); + } + } + + return message; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java index 2ef441d3..cccdfe55 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -23,22 +23,15 @@ 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.edac.ReedSolomon_63_35_29_P25; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS; -import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Fast Associated Control CHannel (FACCH) timeslot carrying a S-OEMI Message */ public class FacchTimeslot extends AbstractSignalingTimeslot { - private final static Logger mLog = LoggerFactory.getLogger(FacchTimeslot.class); private static final int MAX_OCTET_INDEX = 144; //156-12 = message length minus CRC-12 checksum. private static final IntField INFO_1 = IntField.range(2, 7); @@ -138,7 +131,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot if(mMacMessages == null) { int[] input = new int[63]; - int[] output = new int[63]; + float[] reliabilities = createReliabilities(); // input[0] = 0; //Punctured // input[1] = 0; //Punctured @@ -149,51 +142,51 @@ public class FacchTimeslot extends AbstractSignalingTimeslot // input[6] = 0; //Punctured // input[7] = 0; //Punctured // input[8] = 0; //Punctured - input[9] = getInt(PARITY_19); - input[10] = getInt(PARITY_18); - input[11] = getInt(PARITY_17); - input[12] = getInt(PARITY_16); - input[13] = getInt(PARITY_15); - input[14] = getInt(PARITY_14); - input[15] = getInt(PARITY_13); - input[16] = getInt(PARITY_12); - input[17] = getInt(PARITY_11); - input[18] = getInt(PARITY_10); - input[19] = getInt(PARITY_9); - input[20] = getInt(PARITY_8); - input[21] = getInt(PARITY_7); - input[22] = getInt(PARITY_6); - input[23] = getInt(PARITY_5); - input[24] = getInt(PARITY_4); - input[25] = getInt(PARITY_3); - input[26] = getInt(PARITY_2); - input[27] = getInt(PARITY_1); - input[28] = getInt(INFO_26); - input[29] = getInt(INFO_25); - input[30] = getInt(INFO_24); - input[31] = getInt(INFO_23); - input[32] = getInt(INFO_22); - input[33] = getInt(INFO_21); - input[34] = getInt(INFO_20); - input[35] = getInt(INFO_19); - input[36] = getInt(INFO_18); - input[37] = getInt(INFO_17); - input[38] = getInt(INFO_16); - input[39] = getInt(INFO_15); - input[40] = getInt(INFO_14); - input[41] = getInt(INFO_13); - input[42] = getInt(INFO_12); - input[43] = getInt(INFO_11); - input[44] = getInt(INFO_10); - input[45] = getInt(INFO_9); - input[46] = getInt(INFO_8); - input[47] = getInt(INFO_7); - input[48] = getInt(INFO_6); - input[49] = getInt(INFO_5); - input[50] = getInt(INFO_4); - input[51] = getInt(INFO_3); - input[52] = getInt(INFO_2); - input[53] = getInt(INFO_1); + load(input, reliabilities, 9, PARITY_19); + load(input, reliabilities, 10, PARITY_18); + load(input, reliabilities, 11, PARITY_17); + load(input, reliabilities, 12, PARITY_16); + load(input, reliabilities, 13, PARITY_15); + load(input, reliabilities, 14, PARITY_14); + load(input, reliabilities, 15, PARITY_13); + load(input, reliabilities, 16, PARITY_12); + load(input, reliabilities, 17, PARITY_11); + load(input, reliabilities, 18, PARITY_10); + load(input, reliabilities, 19, PARITY_9); + load(input, reliabilities, 20, PARITY_8); + load(input, reliabilities, 21, PARITY_7); + load(input, reliabilities, 22, PARITY_6); + load(input, reliabilities, 23, PARITY_5); + load(input, reliabilities, 24, PARITY_4); + load(input, reliabilities, 25, PARITY_3); + load(input, reliabilities, 26, PARITY_2); + load(input, reliabilities, 27, PARITY_1); + load(input, reliabilities, 28, INFO_26); + load(input, reliabilities, 29, INFO_25); + load(input, reliabilities, 30, INFO_24); + load(input, reliabilities, 31, INFO_23); + load(input, reliabilities, 32, INFO_22); + load(input, reliabilities, 33, INFO_21); + load(input, reliabilities, 34, INFO_20); + load(input, reliabilities, 35, INFO_19); + load(input, reliabilities, 36, INFO_18); + load(input, reliabilities, 37, INFO_17); + load(input, reliabilities, 38, INFO_16); + load(input, reliabilities, 39, INFO_15); + load(input, reliabilities, 40, INFO_14); + load(input, reliabilities, 41, INFO_13); + load(input, reliabilities, 42, INFO_12); + load(input, reliabilities, 43, INFO_11); + load(input, reliabilities, 44, INFO_10); + load(input, reliabilities, 45, INFO_9); + load(input, reliabilities, 46, INFO_8); + load(input, reliabilities, 47, INFO_7); + load(input, reliabilities, 48, INFO_6); + load(input, reliabilities, 49, INFO_5); + load(input, reliabilities, 50, INFO_4); + load(input, reliabilities, 51, INFO_3); + load(input, reliabilities, 52, INFO_2); + load(input, reliabilities, 53, INFO_1); // input[54] = 0; //Shortened // input[55] = 0; //Shortened // input[56] = 0; //Shortened @@ -204,56 +197,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot // input[61] = 0; //Shortened // input[62] = 0; //Shortened - ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25(); - - boolean irrecoverableErrors; - - try - { - irrecoverableErrors = reedSolomon_63_35_29.decode(input, output); - } - catch(Exception e) - { - mLog.error("Error", e); - irrecoverableErrors = true; - } - - CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(156); - - int pointer = 0; - - for(int x = 53; x >= 28; x--) - { - if(output[x] != -1) - { - binaryMessage.load(pointer, 6, output[x]); - } - - pointer += 6; - } - - if(irrecoverableErrors) - { - MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), - new MacStructureFailedRS(binaryMessage, 0)); - macMessage.setValid(false); - mMacMessages = new ArrayList<>(); - mMacMessages.add(macMessage); - } - else - { - //If we corrected any bit errors, update the original message with the bit error count - for(int x = 9; x <= 53; x++) - { - if(output[x] != input[x]) - { - binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x]))); - } - } - } - - mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), - MAX_OCTET_INDEX); + mMacMessages = decode(input, reliabilities, 9, 53, MAX_OCTET_INDEX); } return mMacMessages; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LcchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LcchTimeslot.java index f0d9d098..7e34441d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LcchTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LcchTimeslot.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -21,22 +21,15 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.IntField; -import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS; -import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Logical Control CHannel (LCCH) timeslot carrying an I-OECI message */ public class LcchTimeslot extends AbstractSignalingTimeslot { - private final static Logger mLog = LoggerFactory.getLogger(LcchTimeslot.class); private static final int MAX_OCTET_INDEX = 152; //180-16-12 = message length minus CRC-16 checksum minus 12-bit NAC. private static final IntField INFO_1 = IntField.range(2, 7); @@ -125,7 +118,7 @@ public class LcchTimeslot extends AbstractSignalingTimeslot if(mMacMessages == null) { int[] input = new int[63]; - int[] output = new int[63]; + float[] reliabilities = createReliabilities(); // input[0] = 0; //Punctured // input[1] = 0; //Punctured @@ -133,114 +126,65 @@ public class LcchTimeslot extends AbstractSignalingTimeslot // input[3] = 0; //Punctured // input[4] = 0; //Punctured // input[5] = 0; //Punctured - input[6] = getInt(PARITY_22); - input[7] = getInt(PARITY_21); - input[8] = getInt(PARITY_20); - input[9] = getInt(PARITY_19); - input[10] = getInt(PARITY_18); - input[11] = getInt(PARITY_17); - input[12] = getInt(PARITY_16); - input[13] = getInt(PARITY_15); - input[14] = getInt(PARITY_14); - input[15] = getInt(PARITY_13); - input[16] = getInt(PARITY_12); - input[17] = getInt(PARITY_11); - input[18] = getInt(PARITY_10); - input[19] = getInt(PARITY_9); - input[20] = getInt(PARITY_8); - input[21] = getInt(PARITY_7); - input[22] = getInt(PARITY_6); - input[23] = getInt(PARITY_5); - input[24] = getInt(PARITY_4); - input[25] = getInt(PARITY_3); - input[26] = getInt(PARITY_2); - input[27] = getInt(PARITY_1); - input[28] = getInt(INFO_30); - input[29] = getInt(INFO_29); - input[30] = getInt(INFO_28); - input[31] = getInt(INFO_27); - input[32] = getInt(INFO_26); - input[33] = getInt(INFO_25); - input[34] = getInt(INFO_24); - input[35] = getInt(INFO_23); - input[36] = getInt(INFO_22); - input[37] = getInt(INFO_21); - input[38] = getInt(INFO_20); - input[39] = getInt(INFO_19); - input[40] = getInt(INFO_18); - input[41] = getInt(INFO_17); - input[42] = getInt(INFO_16); - input[43] = getInt(INFO_15); - input[44] = getInt(INFO_14); - input[45] = getInt(INFO_13); - input[46] = getInt(INFO_12); - input[47] = getInt(INFO_11); - input[48] = getInt(INFO_10); - input[49] = getInt(INFO_9); - input[50] = getInt(INFO_8); - input[51] = getInt(INFO_7); - input[52] = getInt(INFO_6); - input[53] = getInt(INFO_5); - input[54] = getInt(INFO_4); - input[55] = getInt(INFO_3); - input[56] = getInt(INFO_2); - input[57] = getInt(INFO_1); + load(input, reliabilities, 6, PARITY_22); + load(input, reliabilities, 7, PARITY_21); + load(input, reliabilities, 8, PARITY_20); + load(input, reliabilities, 9, PARITY_19); + load(input, reliabilities, 10, PARITY_18); + load(input, reliabilities, 11, PARITY_17); + load(input, reliabilities, 12, PARITY_16); + load(input, reliabilities, 13, PARITY_15); + load(input, reliabilities, 14, PARITY_14); + load(input, reliabilities, 15, PARITY_13); + load(input, reliabilities, 16, PARITY_12); + load(input, reliabilities, 17, PARITY_11); + load(input, reliabilities, 18, PARITY_10); + load(input, reliabilities, 19, PARITY_9); + load(input, reliabilities, 20, PARITY_8); + load(input, reliabilities, 21, PARITY_7); + load(input, reliabilities, 22, PARITY_6); + load(input, reliabilities, 23, PARITY_5); + load(input, reliabilities, 24, PARITY_4); + load(input, reliabilities, 25, PARITY_3); + load(input, reliabilities, 26, PARITY_2); + load(input, reliabilities, 27, PARITY_1); + load(input, reliabilities, 28, INFO_30); + load(input, reliabilities, 29, INFO_29); + load(input, reliabilities, 30, INFO_28); + load(input, reliabilities, 31, INFO_27); + load(input, reliabilities, 32, INFO_26); + load(input, reliabilities, 33, INFO_25); + load(input, reliabilities, 34, INFO_24); + load(input, reliabilities, 35, INFO_23); + load(input, reliabilities, 36, INFO_22); + load(input, reliabilities, 37, INFO_21); + load(input, reliabilities, 38, INFO_20); + load(input, reliabilities, 39, INFO_19); + load(input, reliabilities, 40, INFO_18); + load(input, reliabilities, 41, INFO_17); + load(input, reliabilities, 42, INFO_16); + load(input, reliabilities, 43, INFO_15); + load(input, reliabilities, 44, INFO_14); + load(input, reliabilities, 45, INFO_13); + load(input, reliabilities, 46, INFO_12); + load(input, reliabilities, 47, INFO_11); + load(input, reliabilities, 48, INFO_10); + load(input, reliabilities, 49, INFO_9); + load(input, reliabilities, 50, INFO_8); + load(input, reliabilities, 51, INFO_7); + load(input, reliabilities, 52, INFO_6); + load(input, reliabilities, 53, INFO_5); + load(input, reliabilities, 54, INFO_4); + load(input, reliabilities, 55, INFO_3); + load(input, reliabilities, 56, INFO_2); + load(input, reliabilities, 57, INFO_1); // input[58] = 0; //Shortened // input[59] = 0; //Shortened // input[60] = 0; //Shortened // input[61] = 0; //Shortened // input[62] = 0; //Shortened - ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25(); - - boolean irrecoverableErrors; - - try - { - irrecoverableErrors = reedSolomon_63_35_29.decode(input, output); - } - catch(Exception e) - { - mLog.error("Error", e); - irrecoverableErrors = true; - } - - CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(180); - - int pointer = 0; - - for(int x = 57; x >= 28; x--) - { - if(output[x] != -1) - { - binaryMessage.load(pointer, 6, output[x]); - } - - pointer += 6; - } - - if(irrecoverableErrors) - { - MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), - new MacStructureFailedRS(binaryMessage, 0)); - macMessage.setValid(false); - mMacMessages = new ArrayList<>(); - mMacMessages.add(macMessage); - } - else - { - //If we corrected any bit errors, update the original message with the bit error count - for(int x = 6; x <= 57; x++) - { - if(output[x] != input[x]) - { - binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x]))); - } - } - - mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), - MAX_OCTET_INDEX); - } + mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX); } return mMacMessages; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java index 69789300..389048c6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -22,12 +22,8 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.IntField; -import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS; -import java.util.ArrayList; import java.util.List; /** @@ -148,7 +144,7 @@ public class SacchTimeslot extends AbstractSignalingTimeslot if(mMacMessages == null) { int[] input = new int[63]; - int[] output = new int[63]; + float[] reliabilities = createReliabilities(); // input[0] = 0; //Punctured // input[1] = 0; //Punctured @@ -156,115 +152,65 @@ public class SacchTimeslot extends AbstractSignalingTimeslot // input[3] = 0; //Punctured // input[4] = 0; //Punctured // input[5] = 0; //Punctured - input[6] = getInt(PARITY_22); - input[7] = getInt(PARITY_21); - input[8] = getInt(PARITY_20); - input[9] = getInt(PARITY_19); - input[10] = getInt(PARITY_18); - input[11] = getInt(PARITY_17); - input[12] = getInt(PARITY_16); - input[13] = getInt(PARITY_15); - input[14] = getInt(PARITY_14); - input[15] = getInt(PARITY_13); - input[16] = getInt(PARITY_12); - input[17] = getInt(PARITY_11); - input[18] = getInt(PARITY_10); - input[19] = getInt(PARITY_9); - input[20] = getInt(PARITY_8); - input[21] = getInt(PARITY_7); - input[22] = getInt(PARITY_6); - input[23] = getInt(PARITY_5); - input[24] = getInt(PARITY_4); - input[25] = getInt(PARITY_3); - input[26] = getInt(PARITY_2); - input[27] = getInt(PARITY_1); - input[28] = getInt(INFO_30); - input[29] = getInt(INFO_29); - input[30] = getInt(INFO_28); - input[31] = getInt(INFO_27); - input[32] = getInt(INFO_26); - input[33] = getInt(INFO_25); - input[34] = getInt(INFO_24); - input[35] = getInt(INFO_23); - input[36] = getInt(INFO_22); - input[37] = getInt(INFO_21); - input[38] = getInt(INFO_20); - input[39] = getInt(INFO_19); - input[40] = getInt(INFO_18); - input[41] = getInt(INFO_17); - input[42] = getInt(INFO_16); - input[43] = getInt(INFO_15); - input[44] = getInt(INFO_14); - input[45] = getInt(INFO_13); - input[46] = getInt(INFO_12); - input[47] = getInt(INFO_11); - input[48] = getInt(INFO_10); - input[49] = getInt(INFO_9); - input[50] = getInt(INFO_8); - input[51] = getInt(INFO_7); - input[52] = getInt(INFO_6); - input[53] = getInt(INFO_5); - input[54] = getInt(INFO_4); - input[55] = getInt(INFO_3); - input[56] = getInt(INFO_2); - input[57] = getInt(INFO_1); + load(input, reliabilities, 6, PARITY_22); + load(input, reliabilities, 7, PARITY_21); + load(input, reliabilities, 8, PARITY_20); + load(input, reliabilities, 9, PARITY_19); + load(input, reliabilities, 10, PARITY_18); + load(input, reliabilities, 11, PARITY_17); + load(input, reliabilities, 12, PARITY_16); + load(input, reliabilities, 13, PARITY_15); + load(input, reliabilities, 14, PARITY_14); + load(input, reliabilities, 15, PARITY_13); + load(input, reliabilities, 16, PARITY_12); + load(input, reliabilities, 17, PARITY_11); + load(input, reliabilities, 18, PARITY_10); + load(input, reliabilities, 19, PARITY_9); + load(input, reliabilities, 20, PARITY_8); + load(input, reliabilities, 21, PARITY_7); + load(input, reliabilities, 22, PARITY_6); + load(input, reliabilities, 23, PARITY_5); + load(input, reliabilities, 24, PARITY_4); + load(input, reliabilities, 25, PARITY_3); + load(input, reliabilities, 26, PARITY_2); + load(input, reliabilities, 27, PARITY_1); + load(input, reliabilities, 28, INFO_30); + load(input, reliabilities, 29, INFO_29); + load(input, reliabilities, 30, INFO_28); + load(input, reliabilities, 31, INFO_27); + load(input, reliabilities, 32, INFO_26); + load(input, reliabilities, 33, INFO_25); + load(input, reliabilities, 34, INFO_24); + load(input, reliabilities, 35, INFO_23); + load(input, reliabilities, 36, INFO_22); + load(input, reliabilities, 37, INFO_21); + load(input, reliabilities, 38, INFO_20); + load(input, reliabilities, 39, INFO_19); + load(input, reliabilities, 40, INFO_18); + load(input, reliabilities, 41, INFO_17); + load(input, reliabilities, 42, INFO_16); + load(input, reliabilities, 43, INFO_15); + load(input, reliabilities, 44, INFO_14); + load(input, reliabilities, 45, INFO_13); + load(input, reliabilities, 46, INFO_12); + load(input, reliabilities, 47, INFO_11); + load(input, reliabilities, 48, INFO_10); + load(input, reliabilities, 49, INFO_9); + load(input, reliabilities, 50, INFO_8); + load(input, reliabilities, 51, INFO_7); + load(input, reliabilities, 52, INFO_6); + load(input, reliabilities, 53, INFO_5); + load(input, reliabilities, 54, INFO_4); + load(input, reliabilities, 55, INFO_3); + load(input, reliabilities, 56, INFO_2); + load(input, reliabilities, 57, INFO_1); // input[58] = 0; //Shortened // input[59] = 0; //Shortened // input[60] = 0; //Shortened // input[61] = 0; //Shortened // input[62] = 0; //Shortened - ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25(); - - boolean irrecoverableErrors; - - try - { - irrecoverableErrors = reedSolomon_63_35_29.decode(input, output); - } - catch(Exception e) - { - e.printStackTrace(); - irrecoverableErrors = true; - } - - CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(180); - - int pointer = 0; - - for(int x = 57; x >= 28; x--) - { - if(output[x] != -1) - { - binaryMessage.load(pointer, 6, output[x]); - } - - pointer += 6; - } - - - if(irrecoverableErrors) - { - MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), - new MacStructureFailedRS(binaryMessage, 0)); - macMessage.setValid(false); - mMacMessages = new ArrayList<>(); - mMacMessages.add(macMessage); - } - else - { - //If we corrected any bit errors, update the original message with the bit error count - for(int x = 6; x <= 57; x++) - { - if(output[x] != input[x]) - { - binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x]))); - } - } - - mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), 99); - - } + mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX); } return mMacMessages; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java index 45fa20f2..00da9a1e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -39,6 +39,35 @@ public class TimeslotFactory */ public static Timeslot getTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence, int timeslot, long timestamp) + { + return getTimeslot(message, null, scramblingSequence, timeslot, timestamp); + } + + /** + * Creates a timeslot parser instance with soft symbols for soft decision decoding of signaling timeslots. + * + * @param message containing 320-bit timeslot data + * @param softSymbols (radians) for each of the 160 dibits in the timeslot, or null + * @param scramblingSequence to unscramble the transmitted message + * @param timeslot to retrieve + * @param timestamp for the message + * @return timeslot parser + */ + public static Timeslot getTimeslot(CorrectedBinaryMessage message, float[] softSymbols, + BinaryMessage scramblingSequence, int timeslot, long timestamp) + { + Timeslot instance = createTimeslot(message, scramblingSequence, timeslot, timestamp); + + if(softSymbols != null && instance instanceof AbstractSignalingTimeslot signalingTimeslot) + { + signalingTimeslot.setSoftSymbols(softSymbols); + } + + return instance; + } + + private static Timeslot createTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence, + int timeslot, long timestamp) { DataUnitID dataUnitID = Timeslot.getDuid(message); diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java index 53cd6074..1644e0fa 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * 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 @@ -44,6 +44,7 @@ import io.github.dsheirer.vector.calibrate.sample.UnpackedSampleConverterCalibra import io.github.dsheirer.vector.calibrate.sync.DMRSoftSyncCalibration; import io.github.dsheirer.vector.calibrate.sync.NXDNSoftSyncCalibration; import io.github.dsheirer.vector.calibrate.sync.P25P1SoftSyncCalibration; +import io.github.dsheirer.vector.calibrate.sync.P25P2SoftSyncCalibration; import io.github.dsheirer.vector.calibrate.window.WindowCalibration; import java.util.ArrayList; import java.util.Collections; @@ -117,6 +118,7 @@ public class CalibrationManager sInstance.add(new MagnitudeCalibration()); sInstance.add(new NXDNSoftSyncCalibration()); sInstance.add(new P25P1SoftSyncCalibration()); + sInstance.add(new P25P2SoftSyncCalibration()); sInstance.add(new RealDcRemovalCalibration()); sInstance.add(new RealHalfBand11TapFilterCalibration()); sInstance.add(new RealHalfBand15TapFilterCalibration()); diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java index 1da677eb..cf557172 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java @@ -34,7 +34,7 @@ public enum CalibrationType AM_DEMODULATOR("AM Demodulator", 1), DC_REMOVAL_REAL("Real DC Removal Filter", 1), - DMR_SOFT_SYNC_DETECTOR("DMR Soft Sync Detector", 1), + DMR_SOFT_SYNC_DETECTOR("DMR Soft Sync Detector", 2), DIFFERENTIAL_DEMODULATOR("DQPSK Demodulator", 1), FILTER_FIR("FIR Filter", 1), FILTER_HALF_BAND_REAL_11_TAP("Real Half-Band Decimation Filter - 11 Tap", 1), @@ -48,12 +48,13 @@ public enum CalibrationType INTERPOLATOR("Interpolator", 1), MAGNITUDE("Magnitude", 1), MIXER_COMPLEX("Complex Mixer", 1), - NXDN_SOFT_SYNC_DETECTOR("NXDN Soft Sync Detector", 2), + NXDN_SOFT_SYNC_DETECTOR("NXDN Soft Sync Detector", 3), OSCILLATOR_REAL("Real Oscillator", 1), - P25P1_SOFT_SYNC_DETECTOR("P25 Phase 1 Soft Sync Detector", 1), - SAMPLE_PACKED_CONVERTER("Packed Sample Converter", 1), - SAMPLE_UNPACKED_INTERLEAVED_ITERATOR("Unpacked Interleaved Sample Iterator", 1), - SAMPLE_UNPACKED_ITERATOR("Unpacked Sample Iterator", 1), + P25P1_SOFT_SYNC_DETECTOR("P25 Phase 1 Soft Sync Detector", 2), + P25P2_SOFT_SYNC_DETECTOR("P25 Phase 2 Soft Sync Detector", 1), + SAMPLE_PACKED_CONVERTER("Packed Sample Converter", 2), + SAMPLE_UNPACKED_INTERLEAVED_ITERATOR("Unpacked Interleaved Sample Iterator", 2), + SAMPLE_UNPACKED_ITERATOR("Unpacked Sample Iterator", 2), SQUELCHING_FM_DEMODULATOR("Squelching FM Demodulator", 1), WINDOW("Window", 1); diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sample/PackedSampleConverterCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sample/PackedSampleConverterCalibration.java index 4eceb7f8..987dad35 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sample/PackedSampleConverterCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sample/PackedSampleConverterCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * 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 @@ -106,7 +106,7 @@ public class PackedSampleConverterCalibration extends Calibration ScalarUnpackedSampleConverter scalor = new ScalarUnpackedSampleConverter(); long count = 0; - long accumulator = 0; + double accumulator = 0.0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { @@ -115,14 +115,14 @@ public class PackedSampleConverterCalibration extends Calibration count++; } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long testVector(ByteBuffer buffer) { VectorUnpackedSampleConverter vector = new VectorUnpackedSampleConverter(); long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -132,6 +132,6 @@ public class PackedSampleConverterCalibration extends Calibration count++; } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedInterleavedSampleConverterCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedInterleavedSampleConverterCalibration.java index 26f202df..0cc2f947 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedInterleavedSampleConverterCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedInterleavedSampleConverterCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * 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 @@ -220,7 +220,7 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration private long calibrateScalar(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { @@ -234,13 +234,13 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long calibrateVector64(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -255,13 +255,13 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long calibrateVector128(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; for(int x = 0; x < ITERATION_DURATION_MS; x++) @@ -276,13 +276,13 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long calibrateVector256(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -297,13 +297,13 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long calibrateVector512(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -319,6 +319,6 @@ public class UnpackedInterleavedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedSampleConverterCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedSampleConverterCalibration.java index 2fa848d3..822d703e 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedSampleConverterCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sample/UnpackedSampleConverterCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * 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 @@ -208,7 +208,7 @@ public class UnpackedSampleConverterCalibration extends Calibration private long testScalar(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -223,13 +223,13 @@ public class UnpackedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long testVector64(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -244,13 +244,13 @@ public class UnpackedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long testVector128(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { @@ -264,13 +264,13 @@ public class UnpackedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long testVector256(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -286,13 +286,13 @@ public class UnpackedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } private long testVector512(short[] samples, short[] residualI, short[] residualQ) { long start = System.currentTimeMillis(); - long accumulator = 0; + double accumulator = 0.0; long count = 0; while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) @@ -308,6 +308,6 @@ public class UnpackedSampleConverterCalibration extends Calibration } } - return count + (accumulator * 0); + return count + (long)(accumulator * 0); } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sync/DMRSoftSyncCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sync/DMRSoftSyncCalibration.java index bed91710..f52c4b45 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sync/DMRSoftSyncCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sync/DMRSoftSyncCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -45,6 +45,9 @@ public class DMRSoftSyncCalibration extends Calibration private final DMRSoftSyncDetector mVectorDetector128 = new DMRSoftSyncDetectorVector128(); private final DMRSoftSyncDetector mVectorDetector256 = new DMRSoftSyncDetectorVector256(); private final DMRSoftSyncDetector mVectorDetector512 = new DMRSoftSyncDetectorVector512(); + //Accumulates the sync detection scores so that the JIT compiler can't eliminate the unused sync detector + //calculations, which otherwise measures the loop overhead instead of the implementation. + private float mScoreAccumulator; /** * Constructs an instance @@ -193,6 +196,7 @@ public class DMRSoftSyncCalibration extends Calibration private long testScalar(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); @@ -200,79 +204,88 @@ public class DMRSoftSyncCalibration extends Calibration { for(float sample : samples) { - mScalarDetector.process(sample); + accumulator += mScalarDetector.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector64(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector64.process(sample); + accumulator += mVectorDetector64.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector128(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector128.process(sample); + accumulator += mVectorDetector128.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector256(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector256.process(sample); + accumulator += mVectorDetector256.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector512(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector512.process(sample); + accumulator += mVectorDetector512.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sync/NXDNSoftSyncCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sync/NXDNSoftSyncCalibration.java index 6e75be99..4f017ced 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sync/NXDNSoftSyncCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sync/NXDNSoftSyncCalibration.java @@ -45,6 +45,9 @@ public class NXDNSoftSyncCalibration extends Calibration private final NXDNSoftSyncDetector mVectorDetector128 = new NXDNSoftSyncDetectorVector128(); private final NXDNSoftSyncDetector mVectorDetector256 = new NXDNSoftSyncDetectorVector256(); private final NXDNSoftSyncDetector mVectorDetector512 = new NXDNSoftSyncDetectorVector512(); + //Accumulates the sync detection scores so that the JIT compiler can't eliminate the unused sync detector + //calculations, which otherwise measures the loop overhead instead of the implementation. + private float mScoreAccumulator; /** * Constructs an instance @@ -193,6 +196,7 @@ public class NXDNSoftSyncCalibration extends Calibration private long testScalar(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); @@ -200,79 +204,88 @@ public class NXDNSoftSyncCalibration extends Calibration { for(float sample : samples) { - mScalarDetector.process(sample); + accumulator += mScalarDetector.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector64(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector64.process(sample); + accumulator += mVectorDetector64.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector128(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector128.process(sample); + accumulator += mVectorDetector128.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector256(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector256.process(sample); + accumulator += mVectorDetector256.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector512(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector512.process(sample); + accumulator += mVectorDetector512.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P1SoftSyncCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P1SoftSyncCalibration.java index ed7cdfe9..cbb01400 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P1SoftSyncCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P1SoftSyncCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * 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 @@ -45,6 +45,9 @@ public class P25P1SoftSyncCalibration extends Calibration private final P25P1SoftSyncDetector mVectorDetector128 = new P25P1SoftSyncDetectorVector128(); private final P25P1SoftSyncDetector mVectorDetector256 = new P25P1SoftSyncDetectorVector256(); private final P25P1SoftSyncDetector mVectorDetector512 = new P25P1SoftSyncDetectorVector512(); + //Accumulates the sync detection scores so that the JIT compiler can't eliminate the unused sync detector + //calculations, which otherwise measures the loop overhead instead of the implementation. + private float mScoreAccumulator; /** * Constructs an instance @@ -193,6 +196,7 @@ public class P25P1SoftSyncCalibration extends Calibration private long testScalar(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); @@ -200,79 +204,88 @@ public class P25P1SoftSyncCalibration extends Calibration { for(float sample : samples) { - mScalarDetector.process(sample); + accumulator += mScalarDetector.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector64(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector64.process(sample); + accumulator += mVectorDetector64.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector128(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector128.process(sample); + accumulator += mVectorDetector128.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector256(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector256.process(sample); + accumulator += mVectorDetector256.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } private long testVector512(float[] samples) { long count = 0; + float accumulator = 0.0f; long start = System.currentTimeMillis(); while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { for(float sample : samples) { - mVectorDetector512.process(sample); + accumulator += mVectorDetector512.process(sample); } count++; } + mScoreAccumulator += accumulator; return count; } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P2SoftSyncCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P2SoftSyncCalibration.java new file mode 100644 index 00000000..7674b01a --- /dev/null +++ b/src/main/java/io/github/dsheirer/vector/calibrate/sync/P25P2SoftSyncCalibration.java @@ -0,0 +1,305 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.vector.calibrate.sync; + +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetector; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorScalar; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorVector128; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorVector256; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorVector512; +import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetectorVector64; +import io.github.dsheirer.vector.calibrate.Calibration; +import io.github.dsheirer.vector.calibrate.CalibrationException; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; +import org.apache.commons.math3.stat.descriptive.moment.Mean; + +/** + * P25 Phase 2 Soft Sync Detector calibration + */ +public class P25P2SoftSyncCalibration extends Calibration +{ + private static final int BUFFER_SIZE = 2048; + private static final int ITERATION_DURATION_MS = 1000; + private static final int WARMUP_ITERATIONS = 5; + private static final int TEST_ITERATIONS = 5; + private final P25P2SoftSyncDetector mScalarDetector = new P25P2SoftSyncDetectorScalar(); + private final P25P2SoftSyncDetector mVectorDetector64 = new P25P2SoftSyncDetectorVector64(); + private final P25P2SoftSyncDetector mVectorDetector128 = new P25P2SoftSyncDetectorVector128(); + private final P25P2SoftSyncDetector mVectorDetector256 = new P25P2SoftSyncDetectorVector256(); + private final P25P2SoftSyncDetector mVectorDetector512 = new P25P2SoftSyncDetectorVector512(); + //Accumulates the sync detection scores so that the JIT compiler can't eliminate the unused sync detector + //calculations, which otherwise measures the loop overhead instead of the implementation. + private float mScoreAccumulator; + + /** + * Constructs an instance + */ + public P25P2SoftSyncCalibration() + { + super(CalibrationType.P25P2_SOFT_SYNC_DETECTOR); + } + + @Override public void calibrate() throws CalibrationException + { + float[] samples = getFloatSamples(BUFFER_SIZE); + + Mean scalarMean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testScalar(samples); + scalarMean.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + Mean vectorMean64 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector64(samples); + vectorMean64.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); + + Mean vectorMean128 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector128(samples); + vectorMean128.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + Mean vectorMean256 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector256(samples); + vectorMean256.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + Mean vectorMean512 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector512(samples); + vectorMean512.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); + + //Start tests + scalarMean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testScalar(samples); + scalarMean.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + vectorMean64.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector64(samples); + vectorMean64.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); + + vectorMean128.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector128(samples); + vectorMean128.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + vectorMean256.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector256(samples); + vectorMean256.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + vectorMean512.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector512(samples); + vectorMean512.increment(score); + } + + mLog.info("P25P2 SOFT SYNC DETECTOR - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); + + Implementation best = Implementation.SCALAR; + double bestScore = scalarMean.getResult(); + + if(vectorMean64.getResult() > bestScore) + { + bestScore = vectorMean64.getResult(); + best = Implementation.VECTOR_SIMD_64; + } + + if(vectorMean128.getResult() > bestScore) + { + bestScore = vectorMean128.getResult(); + best = Implementation.VECTOR_SIMD_128; + } + + if(vectorMean256.getResult() > bestScore) + { + bestScore = vectorMean256.getResult(); + best = Implementation.VECTOR_SIMD_256; + } + + if(vectorMean512.getResult() > bestScore) + { + best = Implementation.VECTOR_SIMD_512; + } + + setImplementation(best); + + mLog.info("P25P2 SOFT SYNC DETECTOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation()); + } + + private long testScalar(float[] samples) + { + long count = 0; + float accumulator = 0.0f; + + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + for(float sample : samples) + { + accumulator += mScalarDetector.process(sample); + } + count++; + } + + mScoreAccumulator += accumulator; + return count; + } + + private long testVector64(float[] samples) + { + long count = 0; + float accumulator = 0.0f; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + for(float sample : samples) + { + accumulator += mVectorDetector64.process(sample); + } + count++; + } + + mScoreAccumulator += accumulator; + return count; + } + + private long testVector128(float[] samples) + { + long count = 0; + float accumulator = 0.0f; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + for(float sample : samples) + { + accumulator += mVectorDetector128.process(sample); + } + count++; + } + + mScoreAccumulator += accumulator; + return count; + } + + private long testVector256(float[] samples) + { + long count = 0; + float accumulator = 0.0f; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + for(float sample : samples) + { + accumulator += mVectorDetector256.process(sample); + } + count++; + } + + mScoreAccumulator += accumulator; + return count; + } + + private long testVector512(float[] samples) + { + long count = 0; + float accumulator = 0.0f; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + for(float sample : samples) + { + accumulator += mVectorDetector512.process(sample); + } + count++; + } + + mScoreAccumulator += accumulator; + return count; + } + + public static void main(String[] args) + { + P25P2SoftSyncCalibration calibration = new P25P2SoftSyncCalibration(); + + try + { + calibration.calibrate(); + } + catch(Exception e) + { + mLog.error("Error during calibration", e); + } + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSMTest.java b/src/test/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulatorTest.java similarity index 93% rename from src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSMTest.java rename to src/test/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulatorTest.java index 7c2a25a1..6d340fec 100644 --- a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DifferentialDemodulatorLSMTest.java +++ b/src/test/java/io/github/dsheirer/dsp/c4fm/DQPSKDifferentialDemodulatorTest.java @@ -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++) diff --git a/src/test/java/io/github/dsheirer/edac/ReedSolomonErasureDecoderTest.java b/src/test/java/io/github/dsheirer/edac/ReedSolomonErasureDecoderTest.java new file mode 100644 index 00000000..3c10f6f4 --- /dev/null +++ b/src/test/java/io/github/dsheirer/edac/ReedSolomonErasureDecoderTest.java @@ -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 + * **************************************************************************** + */ + +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); + } + } + } +} diff --git a/src/test/java/io/github/dsheirer/edac/ReedSolomonTestEncoder.java b/src/test/java/io/github/dsheirer/edac/ReedSolomonTestEncoder.java new file mode 100644 index 00000000..709214d9 --- /dev/null +++ b/src/test/java/io/github/dsheirer/edac/ReedSolomonTestEncoder.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java index b82bd51f..b6958c27 100644 --- a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java @@ -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 getFrame(BitSet tsbk) { List 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)); diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorTest.java new file mode 100644 index 00000000..861e5c75 --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorTest.java @@ -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 + * **************************************************************************** + */ + +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); + } + } + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKTest.java new file mode 100644 index 00000000..f1c662bf --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKTest.java @@ -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 + * **************************************************************************** + */ + +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 macs = new ArrayList<>(); + Set transmitted = new HashSet<>(); + List dibits = new ArrayList<>(); + + for(int fragment = 0; fragment < FRAGMENT_COUNT + TRAILING_FRAGMENT_COUNT; fragment++) + { + List 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 received = new ArrayList<>(); + List 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 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 buffers, Set transmitted) + { + Set 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 getFragment(int fragment, List macs, ScramblingSequence scramblingSequence, + boolean corruptIsch, Random random) + { + int ischSequence = fragment % 3; + int superframeSequence = (fragment / 3) % 4; + List 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 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 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 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) {} +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequenceTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequenceTest.java new file mode 100644 index 00000000..a3daaaae --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/ISCHSequenceTest.java @@ -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 + * **************************************************************************** + */ + +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)); + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessorTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessorTest.java new file mode 100644 index 00000000..dac10895 --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessorTest.java @@ -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 + * **************************************************************************** + */ + +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()); + } + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoderTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoderTest.java new file mode 100644 index 00000000..fe7184bf --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/message/isch/ISCHDecoderTest.java @@ -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 + * **************************************************************************** + */ + +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 + "]"); + } + } + } +} diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorTest.java new file mode 100644 index 00000000..1922d01c --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase2/sync/P25P2SoftSyncDetectorTest.java @@ -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 + * **************************************************************************** + */ + +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); + } + } + } +}