mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
* Adopts same architecture and symbol processor as P1 C4FM/LSM, DMR and NXDN. * Soft symbol sync detection, processing and erro correction. * Scalar and vector soft sync detection and calibration * Improved time slot detection/prediction and ISCH processing. * Fixes bug in parsing sequential mac structures in SACCH * Vector calibration(s) cleanup across multiple calibrations * Error correction bug in ISCH * Renames differential demod shared with P1 LSM decoder. Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -17,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.FilterFactory;
|
||||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
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
|
* Differential QPSK (ie P25 Phase 1 LSM/CQPSK and P25 Phase 2 HDQPSK) demodulator that produces a real valued sample
|
||||||
* processor. Each output sample is the phase change across the preceding symbol period, in radians per sample (ie
|
* 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
|
* 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.
|
* 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
|
* 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.
|
* 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
|
* The fourth power estimator is ambiguous modulo PI/2 radians per symbol (1200 Hz at 4800 baud, 1500 Hz at 6000 baud),
|
||||||
* resolves offsets within +/- 600 Hz. When a signal is present (ie the fourth power coherence exceeds the noise
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* (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
|
* synchronized. Once an offset range is verified by the decoder (ie a valid P25 Phase 1 NID or a valid P25 Phase 2
|
||||||
* longer dwell period before stepping away from it. Symbol processor synchronization alone doesn't verify the offset
|
* MAC message, see offsetVerified()), the search uses a longer dwell period before stepping away from it. Symbol
|
||||||
* range since a false sync detection in an incorrect offset range would otherwise hold the search in that range for the
|
* processor synchronization alone doesn't verify the offset range since a false sync detection in an incorrect offset
|
||||||
* longer dwell period.
|
* 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
|
* 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
|
* 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
|
* represents the residual offset. Use getFrequencyOffset() to include the coarse offset when reporting the carrier
|
||||||
* offset to the tuner.
|
* offset to the tuner.
|
||||||
*/
|
*/
|
||||||
public class P25P1DifferentialDemodulatorLSM
|
public class DQPSKDifferentialDemodulator
|
||||||
{
|
{
|
||||||
private static final double TWO_PI = 2.0 * Math.PI;
|
private static final double TWO_PI = 2.0 * Math.PI;
|
||||||
//Carrier offset ambiguity step of the fourth power estimator, in radians per symbol
|
//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 mSearchIndex = 0;
|
||||||
private int mSearchDirection = 1;
|
private int mSearchDirection = 1;
|
||||||
private double mUnsynchronizedSignalSymbols = 0;
|
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 boolean mSearchIndexVerified = false;
|
||||||
private double mAverageCoherence = 0.0;
|
private double mAverageCoherence = 0.0;
|
||||||
//Block length in samples and the fourth power accumulation for the block under accumulation
|
//Block length in samples and the fourth power accumulation for the block under accumulation
|
||||||
@@ -130,7 +130,7 @@ public class P25P1DifferentialDemodulatorLSM
|
|||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* 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
|
* Notification that the decoder verified the current carrier offset range (ie a valid NID or MAC message was
|
||||||
* uses the longer dwell period before stepping away from it.
|
* decoded) so that the ambiguity search uses the longer dwell period before stepping away from it.
|
||||||
*/
|
*/
|
||||||
public void offsetVerified()
|
public void offsetVerified()
|
||||||
{
|
{
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
|
||||||
* * *****************************************************************************
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
|
||||||
* * *****************************************************************************
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -225,7 +225,8 @@ public class P25P2Viewer extends VBox
|
|||||||
|
|
||||||
ThreadPool.CACHED.submit(() -> {
|
ThreadPool.CACHED.submit(() -> {
|
||||||
List<MessagePackage> messages = new ArrayList<>();
|
List<MessagePackage> messages = new ArrayList<>();
|
||||||
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null);
|
P25P2MessageFramer messageFramer = new P25P2MessageFramer();
|
||||||
|
messageFramer.start();
|
||||||
messageFramer.setScrambleParameters(scrambleParameters);
|
messageFramer.setScrambleParameters(scrambleParameters);
|
||||||
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
||||||
messageFramer.setListener(messageProcessor);
|
messageFramer.setListener(messageProcessor);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package io.github.dsheirer.module.decode.p25.phase1;
|
package io.github.dsheirer.module.decode.p25.phase1;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.AliasList;
|
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.FilterFactory;
|
||||||
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
|
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
|
||||||
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
|
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 static final int SYMBOL_RATE = 4800;
|
||||||
|
|
||||||
private final P25P1SymbolProcessorLSM mSymbolProcessor;
|
private final P25P1SymbolProcessorLSM mSymbolProcessor;
|
||||||
private final P25P1DifferentialDemodulatorLSM mDemodulator = new P25P1DifferentialDemodulatorLSM();
|
private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator();
|
||||||
private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer();
|
private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer();
|
||||||
private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor();
|
private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor();
|
||||||
private IRealDecimationFilter mDecimationFilterI;
|
private IRealDecimationFilter mDecimationFilterI;
|
||||||
|
|||||||
@@ -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
|
* 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.
|
* 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
|
* Note: testing against LSM recordings without injected noise showed no meaningful difference across equalizer spans
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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;
|
package io.github.dsheirer.module.decode.p25.phase1.sync;
|
||||||
|
|
||||||
import jdk.incubator.vector.FloatVector;
|
import jdk.incubator.vector.FloatVector;
|
||||||
import jdk.incubator.vector.VectorMask;
|
|
||||||
import jdk.incubator.vector.VectorOperators;
|
import jdk.incubator.vector.VectorOperators;
|
||||||
import jdk.incubator.vector.VectorSpecies;
|
import jdk.incubator.vector.VectorSpecies;
|
||||||
|
|
||||||
@@ -29,27 +28,33 @@ import jdk.incubator.vector.VectorSpecies;
|
|||||||
*/
|
*/
|
||||||
public class P25P1SoftSyncDetectorVector512 extends P25P1SoftSyncDetector
|
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<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
|
private static final VectorSpecies<Float> 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
|
* Constructs an instance
|
||||||
* symbols that include this recent soft symbol.
|
*/
|
||||||
|
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.
|
* @return correlation score.
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public float calculate()
|
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
|
//There are 24 symbols that we load into 2x vectors of 16. The second vector overlaps the first vector by 8
|
||||||
//remaining 8 symbols into vector 2 using a vector mask that ignores the first 8 symbols and loads the second 8
|
//symbols (8-15), so it is multiplied against the final 8 pattern symbols (16-23) zero padded in the first 8
|
||||||
// symbols. This sets the first 8 symbols to zero and those values are ignored during calculations.
|
//lanes. Multiplying against zero is cheaper than a masked load.
|
||||||
VectorMask<Float> mask = VectorMask.fromArray(VECTOR_SPECIES, VECTOR_MASK, 0);
|
FloatVector accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer)
|
||||||
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
|
.mul(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0));
|
||||||
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer)
|
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8)
|
||||||
.fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0), accumulator);
|
.fma(FloatVector.fromArray(VECTOR_SPECIES, mTailPatternSymbols, 0), accumulator);
|
||||||
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8, mask)
|
|
||||||
.fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 8), accumulator);
|
|
||||||
return accumulator.reduceLanes(VectorOperators.ADD);
|
return accumulator.reduceLanes(VectorOperators.ADD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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
|
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
|
* Constructs an instance
|
||||||
*/
|
*/
|
||||||
public P25P1SyncDetector()
|
public P25P1SyncDetector()
|
||||||
{
|
{
|
||||||
super(0x5575F5FF77FFL, 24);
|
super(SYNC_PATTERN, 24);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
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<ComplexSamples>, IByteBufferProvider
|
|
||||||
{
|
|
||||||
private double mSampleRate;
|
|
||||||
private final Broadcaster<Dibit> 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<Dibit> getDibitBroadcaster()
|
|
||||||
{
|
|
||||||
return mDibitBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setBufferListener(Listener<ByteBuffer> listener)
|
|
||||||
{
|
|
||||||
mByteBufferAssembler.setBufferListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void removeBufferListener(Listener<ByteBuffer> 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<SourceEvent> 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<ComplexSamples> getComplexSamplesListener()
|
|
||||||
{
|
|
||||||
return P25P2Decoder.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DecoderType getDecoderType()
|
|
||||||
{
|
|
||||||
return DecoderType.P25_PHASE2;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected P25P2MessageProcessor getMessageProcessor()
|
|
||||||
{
|
|
||||||
return mMessageProcessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,24 +18,24 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.module.decode.p25.phase2;
|
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.AliasList;
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.channel.state.MultiChannelState;
|
import io.github.dsheirer.channel.state.MultiChannelState;
|
||||||
import io.github.dsheirer.controller.channel.Channel;
|
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.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.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.Form;
|
||||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||||
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
||||||
import io.github.dsheirer.module.ProcessingChain;
|
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.P25TrafficChannelManager;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
|
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
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.module.decode.p25.phase2.message.mac.MacMessage;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.sample.Listener;
|
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.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.SourceEvent;
|
||||||
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -54,85 +58,119 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
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<ComplexSamples>, IByteBufferProvider, IdentifierUpdateListener
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(P25P2DecoderHDQPSK.class);
|
private final static Logger mLog = LoggerFactory.getLogger(P25P2DecoderHDQPSK.class);
|
||||||
protected static final float SYMBOL_TIMING_GAIN = 0.1f;
|
//Baseband filter settings. The filter precedes the differential demodulator's carrier offset removal, so the pass
|
||||||
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
//band must accommodate the carrier offset. Testing against Phase 2 recordings (pass/stop: total valid MAC messages)
|
||||||
protected DQPSKGardnerDemodulator mQPSKDemodulator;
|
//5500/7000: 9210, 6000/7500: 9273, 6500/7800: 9294, 7000/8500: 9262. Narrower pass bands lost the most valid
|
||||||
protected CostasLoop mCostasLoop;
|
//messages on a recording with a 1.8 kHz carrier offset.
|
||||||
protected P25P2MessageFramer mMessageFramer;
|
private static final double BASEBAND_FILTER_PASS_BAND = 6500.0; //Hertz
|
||||||
protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl();
|
private static final double BASEBAND_FILTER_STOP_BAND = 7800.0; //Hertz
|
||||||
protected IRealFilter mIBasebandFilter;
|
private static final int SYMBOL_RATE = 6000;
|
||||||
protected IRealFilter mQBasebandFilter;
|
private final P25P2SymbolProcessor mSymbolProcessor;
|
||||||
private DecodeConfigP25Phase2 mDecodeConfigP25Phase2;
|
private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator();
|
||||||
private FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
|
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)
|
public P25P2DecoderHDQPSK(DecodeConfigP25Phase2 decodeConfigP25Phase2)
|
||||||
{
|
{
|
||||||
super(6000.0);
|
|
||||||
setSampleRate(25000.0);
|
|
||||||
mDecodeConfigP25Phase2 = decodeConfigP25Phase2;
|
mDecodeConfigP25Phase2 = decodeConfigP25Phase2;
|
||||||
}
|
mMessageProcessor.setMessageListener(getMessageListener());
|
||||||
|
mMessageFramer.setListener(mMessageProcessor);
|
||||||
|
mSymbolProcessor = new P25P2SymbolProcessor(this, mMessageFramer);
|
||||||
|
|
||||||
@Override
|
if(mDecodeConfigP25Phase2 != null)
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters());
|
mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSampleRate(25000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop()
|
public String getProtocolDescription()
|
||||||
{
|
{
|
||||||
super.stop();
|
return "P25 Phase 2";
|
||||||
mQPSKDemodulator.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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)
|
public void setSampleRate(double sampleRate)
|
||||||
{
|
{
|
||||||
super.setSampleRate(sampleRate);
|
if(sampleRate <= SYMBOL_RATE * 2)
|
||||||
|
|
||||||
//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)
|
|
||||||
{
|
{
|
||||||
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.
|
int decimation = 1;
|
||||||
//The PLL gain monitor receives sync detect/loss signals from the message framer
|
|
||||||
mMessageFramer = new P25P2MessageFramer(mCostasLoop);
|
|
||||||
|
|
||||||
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);
|
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
|
||||||
mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
|
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
|
||||||
mMessageFramer.setListener(getMessageProcessor());
|
|
||||||
mMessageFramer.setSampleRate(sampleRate);
|
|
||||||
|
|
||||||
mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
|
float decimatedSampleRate = (float)sampleRate / decimation;
|
||||||
getDibitBroadcaster().addListener(mMessageFramer);
|
|
||||||
|
//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
|
@Override
|
||||||
public void receive(ComplexSamples samples)
|
public void receive(ComplexSamples samples)
|
||||||
{
|
{
|
||||||
float[] i = mIBasebandFilter.filter(samples.i());
|
//Update the message framer with the timestamp from the incoming sample buffer.
|
||||||
float[] q = mQBasebandFilter.filter(samples.q());
|
mMessageFramer.setTimestamp(samples.timestamp());
|
||||||
|
|
||||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
float[] i = mDecimationFilterI.decimateReal(samples.i());
|
||||||
mQPSKDemodulator.receive(amplified);
|
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<ByteBuffer> listener)
|
||||||
|
{
|
||||||
|
mSymbolProcessor.setBufferListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the IByteBufferProvider interface - delegates to the symbol processor
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void removeBufferListener(Listener<ByteBuffer> listener)
|
||||||
|
{
|
||||||
|
setBufferListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the IByteBufferProvider interface - delegates to the symbol processor
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean hasBufferListeners()
|
||||||
|
{
|
||||||
|
return mSymbolProcessor.hasBufferListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void process(SourceEvent sourceEvent)
|
public Listener<SourceEvent> getSourceEventListener()
|
||||||
|
{
|
||||||
|
return this::process;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to receive reusable complex buffers
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Listener<ComplexSamples> 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())
|
switch(sourceEvent.getEvent())
|
||||||
{
|
{
|
||||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||||
mCostasLoop.reset();
|
mSymbolProcessor.resetBalance();
|
||||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
mDemodulator.reset();
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||||
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
|
//Tuner PPM changes are small and the carrier offset estimator and symbol processor balance track the
|
||||||
mCostasLoop.reset();
|
//change, so a reset would only discard the carrier offset estimate.
|
||||||
|
break;
|
||||||
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||||
|
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,22 +302,18 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
|
|||||||
@Override
|
@Override
|
||||||
public void reset()
|
public void reset()
|
||||||
{
|
{
|
||||||
mCostasLoop.reset();
|
mSymbolProcessor.resetBalance();
|
||||||
|
mDemodulator.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
|
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
|
||||||
{
|
{
|
||||||
return new Listener<IdentifierUpdateNotification>()
|
return identifierUpdateNotification -> {
|
||||||
{
|
if(identifierUpdateNotification.getIdentifier().getForm() == Form.SCRAMBLE_PARAMETERS)
|
||||||
@Override
|
|
||||||
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
|
||||||
{
|
{
|
||||||
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();
|
DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2();
|
||||||
channel.setDecodeConfiguration(config);
|
channel.setDecodeConfiguration(config);
|
||||||
|
|
||||||
Path path = Paths.get("/home/denny/temp/20240309_054833_855787500_Duke_Energy_Monroe_Dicks_Creek_Site_201-24_1_baseband.wav");
|
Path directory = Paths.get("/run/media/denny/T9/P25P2 Baseband Samples/");
|
||||||
// ScrambleParameters scrambleParameters = new ScrambleParameters(0x91F14, 0x201, 0x00A);
|
Path path = directory.resolve("20240303_203244_859412500_Duke_Energy_P25_Lake_Control_28_baseband.wav");
|
||||||
// config.setScrambleParameters(scrambleParameters);
|
|
||||||
|
// 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");
|
AliasList aliasList = new AliasList("bogus");
|
||||||
ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel());
|
ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel());
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.AliasList;
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.controller.channel.Channel;
|
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.Dibit;
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
|
||||||
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
||||||
import io.github.dsheirer.log.ApplicationLog;
|
import io.github.dsheirer.log.ApplicationLog;
|
||||||
import io.github.dsheirer.message.IMessage;
|
import io.github.dsheirer.message.IMessage;
|
||||||
@@ -49,52 +48,29 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors
|
* P25 Phase 2 message framer. Receives symbol decisions (dibits) and sync detection notifications from the symbol
|
||||||
* and issue phase corrections.
|
* processor and frames super frame fragments using the super frame detector.
|
||||||
*/
|
*/
|
||||||
public class P25P2MessageFramer implements Listener<Dibit>
|
public class P25P2MessageFramer extends C4FMMessageFramer
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(P25P2MessageFramer.class);
|
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
|
* 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
|
* Sets or updates the scramble parameters for the current channel
|
||||||
* @param scrambleParameters
|
* @param scrambleParameters to apply
|
||||||
*/
|
*/
|
||||||
public void setScrambleParameters(ScrambleParameters scrambleParameters)
|
public void setScrambleParameters(ScrambleParameters scrambleParameters)
|
||||||
{
|
{
|
||||||
if(mSuperFrameDetector != null)
|
mSuperFrameDetector.setScrambleParameters(scrambleParameters);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,22 +80,87 @@ public class P25P2MessageFramer implements Listener<Dibit>
|
|||||||
*/
|
*/
|
||||||
public void setListener(Listener<IMessage> messageListener)
|
public void setListener(Listener<IMessage> 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.
|
* Primary method for streaming decoded symbol dibits for message framing.
|
||||||
*
|
*
|
||||||
* @param dibit to process
|
* @param dibit to process
|
||||||
|
* @return true if a valid super frame fragment was produced.
|
||||||
*/
|
*/
|
||||||
@Override
|
@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.
|
* @param buffer to process into a stream of dibits for processing.
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +170,7 @@ public class P25P2MessageFramer implements Listener<Dibit>
|
|||||||
{
|
{
|
||||||
for(int x = 0; x <= 3; x++)
|
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<Dibit>
|
|||||||
|
|
||||||
processingChain.start();
|
processingChain.start();
|
||||||
|
|
||||||
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null);
|
P25P2MessageFramer messageFramer = new P25P2MessageFramer();
|
||||||
|
messageFramer.start();
|
||||||
messageFramer.setScrambleParameters(scrambleParameters);
|
messageFramer.setScrambleParameters(scrambleParameters);
|
||||||
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
||||||
messageFramer.setListener(messageProcessor);
|
messageFramer.setListener(messageProcessor);
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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;
|
package io.github.dsheirer.module.decode.p25.phase2;
|
||||||
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
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.Dibit;
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
|
||||||
import io.github.dsheirer.message.IMessage;
|
import io.github.dsheirer.message.IMessage;
|
||||||
import io.github.dsheirer.message.SyncLossMessage;
|
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.enumeration.ScrambleParameters;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.SuperFrameFragment;
|
import io.github.dsheirer.module.decode.p25.phase2.message.SuperFrameFragment;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
||||||
@@ -40,14 +39,16 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APCO25 Phase 2 super-frame fragment detector uses a pair of sync pattern detectors and a circular dibit buffer to
|
* APCO25 Phase 2 super-frame fragment detector uses sync detection notifications and a circular dibit buffer to
|
||||||
* detect sync patterns and correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and
|
* correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and 4x ISCH fragments. Sync
|
||||||
* 4x ISCH fragments. The first ISCH fragment consisting of 20-bits from the preceding fragment and 20-bits from
|
* 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:
|
* 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
|
* I-ISCH1 + TIMESLOT1 + I-ISCH2 + TIMESLOT2 + S-SISCH1(sync1) + TIMESLOT3 + S-SISCH2(sync2) + TIMESLOT4
|
||||||
*/
|
*/
|
||||||
public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectListener
|
public class P25P2SuperFrameDetector
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(P25P2SuperFrameDetector.class);
|
private final static Logger mLog = LoggerFactory.getLogger(P25P2SuperFrameDetector.class);
|
||||||
|
|
||||||
@@ -70,8 +71,8 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
|
|||||||
private static final int DIBIT_COUNT_MISALIGNED_SYNC = FRAGMENT_DIBIT_LENGTH - 180;
|
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)
|
* Threshold for broadcasting a sync loss message every 3000 dibits (0.5 seconds at 6000 dibits per second) when not
|
||||||
* plus the length of one super frame (720).
|
* 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;
|
private static final int BROADCAST_SYNC_LOSS_DIBIT_COUNT = 3720;
|
||||||
|
|
||||||
@@ -98,14 +99,34 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
|
|||||||
*/
|
*/
|
||||||
private static final int UN_SYNCHRONIZED_SYNC_MATCH_THRESHOLD = 4;
|
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 ScramblingSequence mScramblingSequence = new ScramblingSequence();
|
||||||
private Listener<IMessage> mMessageListener;
|
private Listener<IMessage> 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
|
* 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<Dibit>, ISyncDetectList
|
|||||||
* stuffed or deleted dibits in the sequence.
|
* stuffed or deleted dibits in the sequence.
|
||||||
*/
|
*/
|
||||||
private DibitDelayBuffer mFragmentBuffer = new DibitDelayBuffer(720 + (2 * FRAGMENT_BUFFER_OVERSIZE));
|
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<Dibit>, ISyncDetectList
|
|||||||
mScramblingSequence.update(scramblingSequence);
|
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<IMessage> listener)
|
public void setListener(Listener<IMessage> listener)
|
||||||
{
|
{
|
||||||
mMessageListener = 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);
|
mReferenceTimestamp = timestamp;
|
||||||
}
|
mDibitsSinceTimestamp = 0;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the timestamp accurate to the currently received dibit.
|
||||||
|
* @return timestamp in milliseconds.
|
||||||
|
*/
|
||||||
private long getCurrentTimestamp()
|
private long getCurrentTimestamp()
|
||||||
{
|
{
|
||||||
//TODO: implement a dibit counter and timestamp calculator. We should receive a timestamp update with each
|
if(mReferenceTimestamp > 0)
|
||||||
//TODO: buffer that arrives. Use the timestamp dibit counter to calculate the exact timestamp of where we're at.
|
{
|
||||||
|
return mReferenceTimestamp + (long)(1000.0 * mDibitsSinceTimestamp / DIBITS_PER_SECOND);
|
||||||
|
}
|
||||||
|
|
||||||
return System.currentTimeMillis();
|
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++;
|
mDibitsProcessed++;
|
||||||
|
mDibitsSinceTimestamp++;
|
||||||
|
mDibitCounter++;
|
||||||
|
|
||||||
mFragmentBuffer.put(dibit);
|
mFragmentBuffer.put(dibit);
|
||||||
|
|
||||||
if(mSynchronized)
|
if(mSynchronized)
|
||||||
{
|
{
|
||||||
mSyncDetectionDelayBuffer.put(dibit);
|
mSyncDetectionCountdown = 0;
|
||||||
|
|
||||||
if(mDibitsProcessed >= FRAGMENT_DIBIT_LENGTH)
|
if(mDibitsProcessed >= FRAGMENT_DIBIT_LENGTH)
|
||||||
{
|
{
|
||||||
checkFragmentSync(0);
|
checkFragmentSync(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if(mSyncDetectionCountdown > 0)
|
||||||
{
|
{
|
||||||
//Only feed the sync pattern detector if we're not synchronized
|
mSyncDetectionCountdown--;
|
||||||
Dibit delayed = mSyncDetectionDelayBuffer.getAndPut(dibit);
|
|
||||||
mSyncDetector.receive(delayed);
|
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)
|
if(mDibitsProcessed > BROADCAST_SYNC_LOSS_DIBIT_COUNT)
|
||||||
{
|
{
|
||||||
mDibitsProcessed -= 3000;
|
mDibitsProcessed -= 3000;
|
||||||
broadcastSyncLoss(3000);
|
broadcastSyncLoss(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mValidFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,8 +335,12 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
|
|||||||
mDibitsProcessed = 0 + dibitOffset;
|
mDibitsProcessed = 0 + dibitOffset;
|
||||||
CorrectedBinaryMessage message = mFragmentBuffer.getMessage(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720);
|
CorrectedBinaryMessage message = mFragmentBuffer.getMessage(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720);
|
||||||
message.setCorrectedBitCount(bitErrors);
|
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);
|
updateScramblingCode(frameFragment);
|
||||||
|
mValidFragment = hasValidMacMessage(frameFragment);
|
||||||
broadcast(frameFragment);
|
broadcast(frameFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +374,51 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
|
|||||||
//Xor the two messages to produce a unified super frame fragment
|
//Xor the two messages to produce a unified super frame fragment
|
||||||
message1.xor(message2);
|
message1.xor(message2);
|
||||||
message1.setCorrectedBitCount(bitErrors); //Not even sure what the correct bit error count is here?
|
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);
|
updateScramblingCode(frameFragment);
|
||||||
|
mValidFragment = hasValidMacMessage(frameFragment);
|
||||||
broadcast(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.
|
* Updates the scrambling code when we receive a network status broadcast with WACN, SYSTEM and NAC.
|
||||||
* @param superFrameFragment
|
* @param superFrameFragment
|
||||||
@@ -316,6 +461,30 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, 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)
|
private void broadcastSyncLoss(int dibitsProcessed)
|
||||||
{
|
{
|
||||||
broadcast(new SyncLossMessage(getCurrentTimestamp(), dibitsProcessed * 2, Protocol.APCO25_PHASE2));
|
broadcast(new SyncLossMessage(getCurrentTimestamp(), dibitsProcessed * 2, Protocol.APCO25_PHASE2));
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
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<Dibit>
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -57,6 +57,22 @@ public enum ISCHSequence
|
|||||||
return mTimeslotOffset;
|
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
|
* Indicates if this sequence is a final fragment sequence
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.BinaryMessage;
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
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.AbstractVoiceTimeslot;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot;
|
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice4Timeslot;
|
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice4Timeslot;
|
||||||
@@ -42,6 +42,10 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class EncryptionSynchronizationSequenceProcessor
|
public class EncryptionSynchronizationSequenceProcessor
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(EncryptionSynchronizationSequenceProcessor.class);
|
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 mESSA;
|
||||||
private BinaryMessage mESSB1;
|
private BinaryMessage mESSB1;
|
||||||
@@ -114,70 +118,51 @@ public class EncryptionSynchronizationSequenceProcessor
|
|||||||
public EncryptionSynchronizationSequence getSequence()
|
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
|
//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
|
//code can correct v symbol errors and e erasures, where 2v + e <= 28, across the 16 info symbols provided by
|
||||||
//provided by ESS-A
|
//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))
|
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
|
//We have to reverse the order of the information hexbits and the RS parity hexbits in the array for
|
||||||
//the RS algorithm.
|
//the RS algorithm.
|
||||||
int[] input = new int[63];
|
int[] input = new int[63];
|
||||||
|
int[] erasures = new int[12];
|
||||||
int inputPointer = 0;
|
int erasureCount = 0;
|
||||||
|
|
||||||
for(int x = 27; x >= 0; x--)
|
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)
|
BinaryMessage[] essB = {mESSB4, mESSB3, mESSB2, mESSB1};
|
||||||
{
|
|
||||||
for(int x = 3; x >= 0; x--)
|
|
||||||
{
|
|
||||||
input[inputPointer++] = mESSB4.getInt(x * 6, x * 6 + 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inputPointer += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mESSB3 != null)
|
for(int fragment = 0; fragment < essB.length; fragment++)
|
||||||
{
|
{
|
||||||
for(int x = 3; x >= 0; x--)
|
int inputPointer = 28 + (fragment * 4);
|
||||||
{
|
|
||||||
input[inputPointer++] = mESSB3.getInt(x * 6, x * 6 + 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inputPointer += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mESSB2 != null)
|
|
||||||
{
|
|
||||||
for(int x = 3; x >= 0; x--)
|
for(int x = 3; x >= 0; x--)
|
||||||
{
|
{
|
||||||
input[inputPointer++] = mESSB2.getInt(x * 6, x * 6 + 5);
|
if(essB[fragment] != null)
|
||||||
}
|
{
|
||||||
}
|
input[inputPointer] = essB[fragment].getInt(x * 6, x * 6 + 5);
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
inputPointer += 4;
|
{
|
||||||
}
|
erasures[erasureCount++] = inputPointer;
|
||||||
|
}
|
||||||
|
|
||||||
if(mESSB1 != null)
|
inputPointer++;
|
||||||
{
|
|
||||||
for(int x = 3; x >= 0; x--)
|
|
||||||
{
|
|
||||||
input[inputPointer++] = mESSB1.getInt(x * 6, x * 6 + 5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] output = new int[63];
|
int[] output = new int[63];
|
||||||
|
boolean irrecoverableErrors = decode(input, output, erasures, 0);
|
||||||
|
|
||||||
ReedSolomon_44_16_29_P25 rs = new ReedSolomon_44_16_29_P25();
|
if(irrecoverableErrors && erasureCount > 0)
|
||||||
|
{
|
||||||
boolean irrecoverableErrors = rs.decode(input, output);
|
irrecoverableErrors = decode(input, output, erasures, erasureCount);
|
||||||
|
}
|
||||||
|
|
||||||
if(!irrecoverableErrors)
|
if(!irrecoverableErrors)
|
||||||
{
|
{
|
||||||
@@ -188,11 +173,7 @@ public class EncryptionSynchronizationSequenceProcessor
|
|||||||
|
|
||||||
for(int x = 43; x >= 28; x--)
|
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;
|
pointer += 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +184,33 @@ public class EncryptionSynchronizationSequenceProcessor
|
|||||||
return null;
|
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
|
* ESS-B fragment 1
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.bits.CorrectedBinaryMessage;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.message.IMessage;
|
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.IISCH;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
|
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.isch.SISCH;
|
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.module.decode.p25.phase2.timeslot.TimeslotFactory;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ public class SuperFrameFragment implements IMessage
|
|||||||
private static final ISCHDecoder ISCH_DECODER = new ISCHDecoder();
|
private static final ISCHDecoder ISCH_DECODER = new ISCHDecoder();
|
||||||
private long mTimestamp;
|
private long mTimestamp;
|
||||||
private CorrectedBinaryMessage mMessage;
|
private CorrectedBinaryMessage mMessage;
|
||||||
|
private float[] mSoftSymbols;
|
||||||
private IISCH mIISCH1;
|
private IISCH mIISCH1;
|
||||||
private IISCH mIISCH2;
|
private IISCH mIISCH2;
|
||||||
private SISCH mSISCH1;
|
private SISCH mSISCH1;
|
||||||
@@ -66,6 +69,7 @@ public class SuperFrameFragment implements IMessage
|
|||||||
private Timeslot mTimeslotD;
|
private Timeslot mTimeslotD;
|
||||||
private List<Timeslot> mTimeslots;
|
private List<Timeslot> mTimeslots;
|
||||||
private ScramblingSequence mScramblingSequence;
|
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
|
* @param timestamp of the final bit of this fragment
|
||||||
*/
|
*/
|
||||||
public SuperFrameFragment(CorrectedBinaryMessage message, long timestamp, ScramblingSequence scramblingSequence)
|
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;
|
mMessage = message;
|
||||||
|
mSoftSymbols = softSymbols;
|
||||||
mTimestamp = timestamp;
|
mTimestamp = timestamp;
|
||||||
mScramblingSequence = scramblingSequence;
|
mScramblingSequence = scramblingSequence;
|
||||||
}
|
}
|
||||||
@@ -118,7 +136,6 @@ public class SuperFrameFragment implements IMessage
|
|||||||
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
|
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
|
||||||
.getSubMessage(CHANNEL_A_ISCH_START, TIMESLOT_A_START), P25P2Message.TIMESLOT_1);
|
.getSubMessage(CHANNEL_A_ISCH_START, TIMESLOT_A_START), P25P2Message.TIMESLOT_1);
|
||||||
mIISCH1 = new IISCH(message, 0, 1, getTimestamp());
|
mIISCH1 = new IISCH(message, 0, 1, getTimestamp());
|
||||||
mIISCH1.setValid(message.getCorrectedBitCount() < 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mIISCH1;
|
return mIISCH1;
|
||||||
@@ -134,7 +151,6 @@ public class SuperFrameFragment implements IMessage
|
|||||||
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
|
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
|
||||||
.getSubMessage(CHANNEL_B_ISCH_START, TIMESLOT_B_START), P25P2Message.TIMESLOT_2);
|
.getSubMessage(CHANNEL_B_ISCH_START, TIMESLOT_B_START), P25P2Message.TIMESLOT_2);
|
||||||
mIISCH2 = new IISCH(message, 0, 2, getTimestamp());
|
mIISCH2 = new IISCH(message, 0, 2, getTimestamp());
|
||||||
mIISCH2.setValid(message.getCorrectedBitCount() < 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mIISCH2;
|
return mIISCH2;
|
||||||
@@ -309,7 +325,46 @@ public class SuperFrameFragment implements IMessage
|
|||||||
{
|
{
|
||||||
CorrectedBinaryMessage message = getMessage().getSubMessage(start, end);
|
CorrectedBinaryMessage message = getMessage().getSubMessage(start, end);
|
||||||
BinaryMessage timeslotSequence = mScramblingSequence.getTimeslotSequence(getTimeslotOffset() + index);
|
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()
|
private int getTimeslotOffset()
|
||||||
{
|
{
|
||||||
if(getIISCH1().isValid())
|
return getIschSequence().getTimeslotOffset();
|
||||||
{
|
|
||||||
return getIISCH1().getIschSequence().getTimeslotOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(getIISCH2().isValid())
|
|
||||||
{
|
|
||||||
return getIISCH2().getIschSequence().getTimeslotOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if this fragment is the final fragment (3 of 3) for a superframe which indicates that the final two
|
* 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()
|
private boolean isFinalFragment()
|
||||||
{
|
{
|
||||||
if(getIISCH1().isValid())
|
return getIschSequence().isFinalFragment();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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,
|
private static final long[] GENERATOR = {0x8816CE36D7l, 0x201DFD4F64l, 0x100F4B1758l, 0x0C00DED18El, 0x020807F7FFl,
|
||||||
0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l};
|
0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l};
|
||||||
private static final long CODE_WORD_OFFSET = 0x184229d461l;
|
private static final long CODE_WORD_OFFSET = 0x184229d461l;
|
||||||
|
private static final int MAX_CORRECTABLE_BIT_ERRORS = 7;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the ISCH-I parsing class
|
* Constructs the ISCH-I parsing class
|
||||||
@@ -93,6 +94,46 @@ public class ISCHDecoder
|
|||||||
return codeword;
|
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<Long,BinaryMessage> 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<Long,BinaryMessage> 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
|
* 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.
|
* only use the codewords that are correct for the expected timeslot.
|
||||||
@@ -105,64 +146,20 @@ public class ISCHDecoder
|
|||||||
|
|
||||||
if(expectedTimeslot == 1)
|
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));
|
return decoded;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(expectedTimeslot == 2)
|
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));
|
return decoded;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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);
|
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
|
* Creates a set of MAC messages
|
||||||
* @param timeslot for the messages
|
* @param timeslot for the messages
|
||||||
@@ -176,26 +200,13 @@ public class MacMessageFactory
|
|||||||
{
|
{
|
||||||
List<MacMessage> messages = new ArrayList<>();
|
List<MacMessage> messages = new ArrayList<>();
|
||||||
|
|
||||||
boolean passesCRC = true;
|
boolean passesCRC = passesCRC(dataUnitID, message);
|
||||||
|
|
||||||
int nac = 0;
|
int nac = 0;
|
||||||
|
|
||||||
if(dataUnitID.isFACCH())
|
if(passesCRC && dataUnitID.isLCCH())
|
||||||
{
|
{
|
||||||
passesCRC = CRCP25.crc12_FACCH(message);
|
nac = MacStructure.getLcchNac(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!passesCRC)
|
if(!passesCRC)
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Float> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Float> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Float> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Float> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* * 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
|
* * 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.BinaryMessage;
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
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.identifier.Identifier;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
|
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.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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -36,6 +41,38 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public abstract class AbstractSignalingTimeslot extends Timeslot
|
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
|
* 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
|
* Access Encoded MAC Information (EMI) message(s) carried by the signalling timeslot
|
||||||
*/
|
*/
|
||||||
public abstract List<MacMessage> getMacMessages();
|
public abstract List<MacMessage> 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<MacMessage> 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<Integer> 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<MacMessage> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.CorrectedBinaryMessage;
|
||||||
import io.github.dsheirer.bits.FragmentedIntField;
|
import io.github.dsheirer.bits.FragmentedIntField;
|
||||||
import io.github.dsheirer.bits.IntField;
|
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.enumeration.DataUnitID;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
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 java.util.List;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast Associated Control CHannel (FACCH) timeslot carrying a S-OEMI Message
|
* Fast Associated Control CHannel (FACCH) timeslot carrying a S-OEMI Message
|
||||||
*/
|
*/
|
||||||
public class FacchTimeslot extends AbstractSignalingTimeslot
|
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 int MAX_OCTET_INDEX = 144; //156-12 = message length minus CRC-12 checksum.
|
||||||
|
|
||||||
private static final IntField INFO_1 = IntField.range(2, 7);
|
private static final IntField INFO_1 = IntField.range(2, 7);
|
||||||
@@ -138,7 +131,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
|
|||||||
if(mMacMessages == null)
|
if(mMacMessages == null)
|
||||||
{
|
{
|
||||||
int[] input = new int[63];
|
int[] input = new int[63];
|
||||||
int[] output = new int[63];
|
float[] reliabilities = createReliabilities();
|
||||||
|
|
||||||
// input[0] = 0; //Punctured
|
// input[0] = 0; //Punctured
|
||||||
// input[1] = 0; //Punctured
|
// input[1] = 0; //Punctured
|
||||||
@@ -149,51 +142,51 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
|
|||||||
// input[6] = 0; //Punctured
|
// input[6] = 0; //Punctured
|
||||||
// input[7] = 0; //Punctured
|
// input[7] = 0; //Punctured
|
||||||
// input[8] = 0; //Punctured
|
// input[8] = 0; //Punctured
|
||||||
input[9] = getInt(PARITY_19);
|
load(input, reliabilities, 9, PARITY_19);
|
||||||
input[10] = getInt(PARITY_18);
|
load(input, reliabilities, 10, PARITY_18);
|
||||||
input[11] = getInt(PARITY_17);
|
load(input, reliabilities, 11, PARITY_17);
|
||||||
input[12] = getInt(PARITY_16);
|
load(input, reliabilities, 12, PARITY_16);
|
||||||
input[13] = getInt(PARITY_15);
|
load(input, reliabilities, 13, PARITY_15);
|
||||||
input[14] = getInt(PARITY_14);
|
load(input, reliabilities, 14, PARITY_14);
|
||||||
input[15] = getInt(PARITY_13);
|
load(input, reliabilities, 15, PARITY_13);
|
||||||
input[16] = getInt(PARITY_12);
|
load(input, reliabilities, 16, PARITY_12);
|
||||||
input[17] = getInt(PARITY_11);
|
load(input, reliabilities, 17, PARITY_11);
|
||||||
input[18] = getInt(PARITY_10);
|
load(input, reliabilities, 18, PARITY_10);
|
||||||
input[19] = getInt(PARITY_9);
|
load(input, reliabilities, 19, PARITY_9);
|
||||||
input[20] = getInt(PARITY_8);
|
load(input, reliabilities, 20, PARITY_8);
|
||||||
input[21] = getInt(PARITY_7);
|
load(input, reliabilities, 21, PARITY_7);
|
||||||
input[22] = getInt(PARITY_6);
|
load(input, reliabilities, 22, PARITY_6);
|
||||||
input[23] = getInt(PARITY_5);
|
load(input, reliabilities, 23, PARITY_5);
|
||||||
input[24] = getInt(PARITY_4);
|
load(input, reliabilities, 24, PARITY_4);
|
||||||
input[25] = getInt(PARITY_3);
|
load(input, reliabilities, 25, PARITY_3);
|
||||||
input[26] = getInt(PARITY_2);
|
load(input, reliabilities, 26, PARITY_2);
|
||||||
input[27] = getInt(PARITY_1);
|
load(input, reliabilities, 27, PARITY_1);
|
||||||
input[28] = getInt(INFO_26);
|
load(input, reliabilities, 28, INFO_26);
|
||||||
input[29] = getInt(INFO_25);
|
load(input, reliabilities, 29, INFO_25);
|
||||||
input[30] = getInt(INFO_24);
|
load(input, reliabilities, 30, INFO_24);
|
||||||
input[31] = getInt(INFO_23);
|
load(input, reliabilities, 31, INFO_23);
|
||||||
input[32] = getInt(INFO_22);
|
load(input, reliabilities, 32, INFO_22);
|
||||||
input[33] = getInt(INFO_21);
|
load(input, reliabilities, 33, INFO_21);
|
||||||
input[34] = getInt(INFO_20);
|
load(input, reliabilities, 34, INFO_20);
|
||||||
input[35] = getInt(INFO_19);
|
load(input, reliabilities, 35, INFO_19);
|
||||||
input[36] = getInt(INFO_18);
|
load(input, reliabilities, 36, INFO_18);
|
||||||
input[37] = getInt(INFO_17);
|
load(input, reliabilities, 37, INFO_17);
|
||||||
input[38] = getInt(INFO_16);
|
load(input, reliabilities, 38, INFO_16);
|
||||||
input[39] = getInt(INFO_15);
|
load(input, reliabilities, 39, INFO_15);
|
||||||
input[40] = getInt(INFO_14);
|
load(input, reliabilities, 40, INFO_14);
|
||||||
input[41] = getInt(INFO_13);
|
load(input, reliabilities, 41, INFO_13);
|
||||||
input[42] = getInt(INFO_12);
|
load(input, reliabilities, 42, INFO_12);
|
||||||
input[43] = getInt(INFO_11);
|
load(input, reliabilities, 43, INFO_11);
|
||||||
input[44] = getInt(INFO_10);
|
load(input, reliabilities, 44, INFO_10);
|
||||||
input[45] = getInt(INFO_9);
|
load(input, reliabilities, 45, INFO_9);
|
||||||
input[46] = getInt(INFO_8);
|
load(input, reliabilities, 46, INFO_8);
|
||||||
input[47] = getInt(INFO_7);
|
load(input, reliabilities, 47, INFO_7);
|
||||||
input[48] = getInt(INFO_6);
|
load(input, reliabilities, 48, INFO_6);
|
||||||
input[49] = getInt(INFO_5);
|
load(input, reliabilities, 49, INFO_5);
|
||||||
input[50] = getInt(INFO_4);
|
load(input, reliabilities, 50, INFO_4);
|
||||||
input[51] = getInt(INFO_3);
|
load(input, reliabilities, 51, INFO_3);
|
||||||
input[52] = getInt(INFO_2);
|
load(input, reliabilities, 52, INFO_2);
|
||||||
input[53] = getInt(INFO_1);
|
load(input, reliabilities, 53, INFO_1);
|
||||||
// input[54] = 0; //Shortened
|
// input[54] = 0; //Shortened
|
||||||
// input[55] = 0; //Shortened
|
// input[55] = 0; //Shortened
|
||||||
// input[56] = 0; //Shortened
|
// input[56] = 0; //Shortened
|
||||||
@@ -204,56 +197,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
|
|||||||
// input[61] = 0; //Shortened
|
// input[61] = 0; //Shortened
|
||||||
// input[62] = 0; //Shortened
|
// input[62] = 0; //Shortened
|
||||||
|
|
||||||
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
|
mMacMessages = decode(input, reliabilities, 9, 53, MAX_OCTET_INDEX);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMacMessages;
|
return mMacMessages;
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.CorrectedBinaryMessage;
|
||||||
import io.github.dsheirer.bits.IntField;
|
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.enumeration.DataUnitID;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
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 java.util.List;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logical Control CHannel (LCCH) timeslot carrying an I-OECI message
|
* Logical Control CHannel (LCCH) timeslot carrying an I-OECI message
|
||||||
*/
|
*/
|
||||||
public class LcchTimeslot extends AbstractSignalingTimeslot
|
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 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);
|
private static final IntField INFO_1 = IntField.range(2, 7);
|
||||||
@@ -125,7 +118,7 @@ public class LcchTimeslot extends AbstractSignalingTimeslot
|
|||||||
if(mMacMessages == null)
|
if(mMacMessages == null)
|
||||||
{
|
{
|
||||||
int[] input = new int[63];
|
int[] input = new int[63];
|
||||||
int[] output = new int[63];
|
float[] reliabilities = createReliabilities();
|
||||||
|
|
||||||
// input[0] = 0; //Punctured
|
// input[0] = 0; //Punctured
|
||||||
// input[1] = 0; //Punctured
|
// input[1] = 0; //Punctured
|
||||||
@@ -133,114 +126,65 @@ public class LcchTimeslot extends AbstractSignalingTimeslot
|
|||||||
// input[3] = 0; //Punctured
|
// input[3] = 0; //Punctured
|
||||||
// input[4] = 0; //Punctured
|
// input[4] = 0; //Punctured
|
||||||
// input[5] = 0; //Punctured
|
// input[5] = 0; //Punctured
|
||||||
input[6] = getInt(PARITY_22);
|
load(input, reliabilities, 6, PARITY_22);
|
||||||
input[7] = getInt(PARITY_21);
|
load(input, reliabilities, 7, PARITY_21);
|
||||||
input[8] = getInt(PARITY_20);
|
load(input, reliabilities, 8, PARITY_20);
|
||||||
input[9] = getInt(PARITY_19);
|
load(input, reliabilities, 9, PARITY_19);
|
||||||
input[10] = getInt(PARITY_18);
|
load(input, reliabilities, 10, PARITY_18);
|
||||||
input[11] = getInt(PARITY_17);
|
load(input, reliabilities, 11, PARITY_17);
|
||||||
input[12] = getInt(PARITY_16);
|
load(input, reliabilities, 12, PARITY_16);
|
||||||
input[13] = getInt(PARITY_15);
|
load(input, reliabilities, 13, PARITY_15);
|
||||||
input[14] = getInt(PARITY_14);
|
load(input, reliabilities, 14, PARITY_14);
|
||||||
input[15] = getInt(PARITY_13);
|
load(input, reliabilities, 15, PARITY_13);
|
||||||
input[16] = getInt(PARITY_12);
|
load(input, reliabilities, 16, PARITY_12);
|
||||||
input[17] = getInt(PARITY_11);
|
load(input, reliabilities, 17, PARITY_11);
|
||||||
input[18] = getInt(PARITY_10);
|
load(input, reliabilities, 18, PARITY_10);
|
||||||
input[19] = getInt(PARITY_9);
|
load(input, reliabilities, 19, PARITY_9);
|
||||||
input[20] = getInt(PARITY_8);
|
load(input, reliabilities, 20, PARITY_8);
|
||||||
input[21] = getInt(PARITY_7);
|
load(input, reliabilities, 21, PARITY_7);
|
||||||
input[22] = getInt(PARITY_6);
|
load(input, reliabilities, 22, PARITY_6);
|
||||||
input[23] = getInt(PARITY_5);
|
load(input, reliabilities, 23, PARITY_5);
|
||||||
input[24] = getInt(PARITY_4);
|
load(input, reliabilities, 24, PARITY_4);
|
||||||
input[25] = getInt(PARITY_3);
|
load(input, reliabilities, 25, PARITY_3);
|
||||||
input[26] = getInt(PARITY_2);
|
load(input, reliabilities, 26, PARITY_2);
|
||||||
input[27] = getInt(PARITY_1);
|
load(input, reliabilities, 27, PARITY_1);
|
||||||
input[28] = getInt(INFO_30);
|
load(input, reliabilities, 28, INFO_30);
|
||||||
input[29] = getInt(INFO_29);
|
load(input, reliabilities, 29, INFO_29);
|
||||||
input[30] = getInt(INFO_28);
|
load(input, reliabilities, 30, INFO_28);
|
||||||
input[31] = getInt(INFO_27);
|
load(input, reliabilities, 31, INFO_27);
|
||||||
input[32] = getInt(INFO_26);
|
load(input, reliabilities, 32, INFO_26);
|
||||||
input[33] = getInt(INFO_25);
|
load(input, reliabilities, 33, INFO_25);
|
||||||
input[34] = getInt(INFO_24);
|
load(input, reliabilities, 34, INFO_24);
|
||||||
input[35] = getInt(INFO_23);
|
load(input, reliabilities, 35, INFO_23);
|
||||||
input[36] = getInt(INFO_22);
|
load(input, reliabilities, 36, INFO_22);
|
||||||
input[37] = getInt(INFO_21);
|
load(input, reliabilities, 37, INFO_21);
|
||||||
input[38] = getInt(INFO_20);
|
load(input, reliabilities, 38, INFO_20);
|
||||||
input[39] = getInt(INFO_19);
|
load(input, reliabilities, 39, INFO_19);
|
||||||
input[40] = getInt(INFO_18);
|
load(input, reliabilities, 40, INFO_18);
|
||||||
input[41] = getInt(INFO_17);
|
load(input, reliabilities, 41, INFO_17);
|
||||||
input[42] = getInt(INFO_16);
|
load(input, reliabilities, 42, INFO_16);
|
||||||
input[43] = getInt(INFO_15);
|
load(input, reliabilities, 43, INFO_15);
|
||||||
input[44] = getInt(INFO_14);
|
load(input, reliabilities, 44, INFO_14);
|
||||||
input[45] = getInt(INFO_13);
|
load(input, reliabilities, 45, INFO_13);
|
||||||
input[46] = getInt(INFO_12);
|
load(input, reliabilities, 46, INFO_12);
|
||||||
input[47] = getInt(INFO_11);
|
load(input, reliabilities, 47, INFO_11);
|
||||||
input[48] = getInt(INFO_10);
|
load(input, reliabilities, 48, INFO_10);
|
||||||
input[49] = getInt(INFO_9);
|
load(input, reliabilities, 49, INFO_9);
|
||||||
input[50] = getInt(INFO_8);
|
load(input, reliabilities, 50, INFO_8);
|
||||||
input[51] = getInt(INFO_7);
|
load(input, reliabilities, 51, INFO_7);
|
||||||
input[52] = getInt(INFO_6);
|
load(input, reliabilities, 52, INFO_6);
|
||||||
input[53] = getInt(INFO_5);
|
load(input, reliabilities, 53, INFO_5);
|
||||||
input[54] = getInt(INFO_4);
|
load(input, reliabilities, 54, INFO_4);
|
||||||
input[55] = getInt(INFO_3);
|
load(input, reliabilities, 55, INFO_3);
|
||||||
input[56] = getInt(INFO_2);
|
load(input, reliabilities, 56, INFO_2);
|
||||||
input[57] = getInt(INFO_1);
|
load(input, reliabilities, 57, INFO_1);
|
||||||
// input[58] = 0; //Shortened
|
// input[58] = 0; //Shortened
|
||||||
// input[59] = 0; //Shortened
|
// input[59] = 0; //Shortened
|
||||||
// input[60] = 0; //Shortened
|
// input[60] = 0; //Shortened
|
||||||
// input[61] = 0; //Shortened
|
// input[61] = 0; //Shortened
|
||||||
// input[62] = 0; //Shortened
|
// input[62] = 0; //Shortened
|
||||||
|
|
||||||
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
|
mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMacMessages;
|
return mMacMessages;
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.BinaryMessage;
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
import io.github.dsheirer.bits.IntField;
|
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.enumeration.DataUnitID;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
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 java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +144,7 @@ public class SacchTimeslot extends AbstractSignalingTimeslot
|
|||||||
if(mMacMessages == null)
|
if(mMacMessages == null)
|
||||||
{
|
{
|
||||||
int[] input = new int[63];
|
int[] input = new int[63];
|
||||||
int[] output = new int[63];
|
float[] reliabilities = createReliabilities();
|
||||||
|
|
||||||
// input[0] = 0; //Punctured
|
// input[0] = 0; //Punctured
|
||||||
// input[1] = 0; //Punctured
|
// input[1] = 0; //Punctured
|
||||||
@@ -156,115 +152,65 @@ public class SacchTimeslot extends AbstractSignalingTimeslot
|
|||||||
// input[3] = 0; //Punctured
|
// input[3] = 0; //Punctured
|
||||||
// input[4] = 0; //Punctured
|
// input[4] = 0; //Punctured
|
||||||
// input[5] = 0; //Punctured
|
// input[5] = 0; //Punctured
|
||||||
input[6] = getInt(PARITY_22);
|
load(input, reliabilities, 6, PARITY_22);
|
||||||
input[7] = getInt(PARITY_21);
|
load(input, reliabilities, 7, PARITY_21);
|
||||||
input[8] = getInt(PARITY_20);
|
load(input, reliabilities, 8, PARITY_20);
|
||||||
input[9] = getInt(PARITY_19);
|
load(input, reliabilities, 9, PARITY_19);
|
||||||
input[10] = getInt(PARITY_18);
|
load(input, reliabilities, 10, PARITY_18);
|
||||||
input[11] = getInt(PARITY_17);
|
load(input, reliabilities, 11, PARITY_17);
|
||||||
input[12] = getInt(PARITY_16);
|
load(input, reliabilities, 12, PARITY_16);
|
||||||
input[13] = getInt(PARITY_15);
|
load(input, reliabilities, 13, PARITY_15);
|
||||||
input[14] = getInt(PARITY_14);
|
load(input, reliabilities, 14, PARITY_14);
|
||||||
input[15] = getInt(PARITY_13);
|
load(input, reliabilities, 15, PARITY_13);
|
||||||
input[16] = getInt(PARITY_12);
|
load(input, reliabilities, 16, PARITY_12);
|
||||||
input[17] = getInt(PARITY_11);
|
load(input, reliabilities, 17, PARITY_11);
|
||||||
input[18] = getInt(PARITY_10);
|
load(input, reliabilities, 18, PARITY_10);
|
||||||
input[19] = getInt(PARITY_9);
|
load(input, reliabilities, 19, PARITY_9);
|
||||||
input[20] = getInt(PARITY_8);
|
load(input, reliabilities, 20, PARITY_8);
|
||||||
input[21] = getInt(PARITY_7);
|
load(input, reliabilities, 21, PARITY_7);
|
||||||
input[22] = getInt(PARITY_6);
|
load(input, reliabilities, 22, PARITY_6);
|
||||||
input[23] = getInt(PARITY_5);
|
load(input, reliabilities, 23, PARITY_5);
|
||||||
input[24] = getInt(PARITY_4);
|
load(input, reliabilities, 24, PARITY_4);
|
||||||
input[25] = getInt(PARITY_3);
|
load(input, reliabilities, 25, PARITY_3);
|
||||||
input[26] = getInt(PARITY_2);
|
load(input, reliabilities, 26, PARITY_2);
|
||||||
input[27] = getInt(PARITY_1);
|
load(input, reliabilities, 27, PARITY_1);
|
||||||
input[28] = getInt(INFO_30);
|
load(input, reliabilities, 28, INFO_30);
|
||||||
input[29] = getInt(INFO_29);
|
load(input, reliabilities, 29, INFO_29);
|
||||||
input[30] = getInt(INFO_28);
|
load(input, reliabilities, 30, INFO_28);
|
||||||
input[31] = getInt(INFO_27);
|
load(input, reliabilities, 31, INFO_27);
|
||||||
input[32] = getInt(INFO_26);
|
load(input, reliabilities, 32, INFO_26);
|
||||||
input[33] = getInt(INFO_25);
|
load(input, reliabilities, 33, INFO_25);
|
||||||
input[34] = getInt(INFO_24);
|
load(input, reliabilities, 34, INFO_24);
|
||||||
input[35] = getInt(INFO_23);
|
load(input, reliabilities, 35, INFO_23);
|
||||||
input[36] = getInt(INFO_22);
|
load(input, reliabilities, 36, INFO_22);
|
||||||
input[37] = getInt(INFO_21);
|
load(input, reliabilities, 37, INFO_21);
|
||||||
input[38] = getInt(INFO_20);
|
load(input, reliabilities, 38, INFO_20);
|
||||||
input[39] = getInt(INFO_19);
|
load(input, reliabilities, 39, INFO_19);
|
||||||
input[40] = getInt(INFO_18);
|
load(input, reliabilities, 40, INFO_18);
|
||||||
input[41] = getInt(INFO_17);
|
load(input, reliabilities, 41, INFO_17);
|
||||||
input[42] = getInt(INFO_16);
|
load(input, reliabilities, 42, INFO_16);
|
||||||
input[43] = getInt(INFO_15);
|
load(input, reliabilities, 43, INFO_15);
|
||||||
input[44] = getInt(INFO_14);
|
load(input, reliabilities, 44, INFO_14);
|
||||||
input[45] = getInt(INFO_13);
|
load(input, reliabilities, 45, INFO_13);
|
||||||
input[46] = getInt(INFO_12);
|
load(input, reliabilities, 46, INFO_12);
|
||||||
input[47] = getInt(INFO_11);
|
load(input, reliabilities, 47, INFO_11);
|
||||||
input[48] = getInt(INFO_10);
|
load(input, reliabilities, 48, INFO_10);
|
||||||
input[49] = getInt(INFO_9);
|
load(input, reliabilities, 49, INFO_9);
|
||||||
input[50] = getInt(INFO_8);
|
load(input, reliabilities, 50, INFO_8);
|
||||||
input[51] = getInt(INFO_7);
|
load(input, reliabilities, 51, INFO_7);
|
||||||
input[52] = getInt(INFO_6);
|
load(input, reliabilities, 52, INFO_6);
|
||||||
input[53] = getInt(INFO_5);
|
load(input, reliabilities, 53, INFO_5);
|
||||||
input[54] = getInt(INFO_4);
|
load(input, reliabilities, 54, INFO_4);
|
||||||
input[55] = getInt(INFO_3);
|
load(input, reliabilities, 55, INFO_3);
|
||||||
input[56] = getInt(INFO_2);
|
load(input, reliabilities, 56, INFO_2);
|
||||||
input[57] = getInt(INFO_1);
|
load(input, reliabilities, 57, INFO_1);
|
||||||
// input[58] = 0; //Shortened
|
// input[58] = 0; //Shortened
|
||||||
// input[59] = 0; //Shortened
|
// input[59] = 0; //Shortened
|
||||||
// input[60] = 0; //Shortened
|
// input[60] = 0; //Shortened
|
||||||
// input[61] = 0; //Shortened
|
// input[61] = 0; //Shortened
|
||||||
// input[62] = 0; //Shortened
|
// input[62] = 0; //Shortened
|
||||||
|
|
||||||
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
|
mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMacMessages;
|
return mMacMessages;
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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,
|
public static Timeslot getTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence,
|
||||||
int timeslot, long timestamp)
|
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);
|
DataUnitID dataUnitID = Timeslot.getDuid(message);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.DMRSoftSyncCalibration;
|
||||||
import io.github.dsheirer.vector.calibrate.sync.NXDNSoftSyncCalibration;
|
import io.github.dsheirer.vector.calibrate.sync.NXDNSoftSyncCalibration;
|
||||||
import io.github.dsheirer.vector.calibrate.sync.P25P1SoftSyncCalibration;
|
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 io.github.dsheirer.vector.calibrate.window.WindowCalibration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -117,6 +118,7 @@ public class CalibrationManager
|
|||||||
sInstance.add(new MagnitudeCalibration());
|
sInstance.add(new MagnitudeCalibration());
|
||||||
sInstance.add(new NXDNSoftSyncCalibration());
|
sInstance.add(new NXDNSoftSyncCalibration());
|
||||||
sInstance.add(new P25P1SoftSyncCalibration());
|
sInstance.add(new P25P1SoftSyncCalibration());
|
||||||
|
sInstance.add(new P25P2SoftSyncCalibration());
|
||||||
sInstance.add(new RealDcRemovalCalibration());
|
sInstance.add(new RealDcRemovalCalibration());
|
||||||
sInstance.add(new RealHalfBand11TapFilterCalibration());
|
sInstance.add(new RealHalfBand11TapFilterCalibration());
|
||||||
sInstance.add(new RealHalfBand15TapFilterCalibration());
|
sInstance.add(new RealHalfBand15TapFilterCalibration());
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public enum CalibrationType
|
|||||||
|
|
||||||
AM_DEMODULATOR("AM Demodulator", 1),
|
AM_DEMODULATOR("AM Demodulator", 1),
|
||||||
DC_REMOVAL_REAL("Real DC Removal Filter", 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),
|
DIFFERENTIAL_DEMODULATOR("DQPSK Demodulator", 1),
|
||||||
FILTER_FIR("FIR Filter", 1),
|
FILTER_FIR("FIR Filter", 1),
|
||||||
FILTER_HALF_BAND_REAL_11_TAP("Real Half-Band Decimation Filter - 11 Tap", 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),
|
INTERPOLATOR("Interpolator", 1),
|
||||||
MAGNITUDE("Magnitude", 1),
|
MAGNITUDE("Magnitude", 1),
|
||||||
MIXER_COMPLEX("Complex Mixer", 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),
|
OSCILLATOR_REAL("Real Oscillator", 1),
|
||||||
P25P1_SOFT_SYNC_DETECTOR("P25 Phase 1 Soft Sync Detector", 1),
|
P25P1_SOFT_SYNC_DETECTOR("P25 Phase 1 Soft Sync Detector", 2),
|
||||||
SAMPLE_PACKED_CONVERTER("Packed Sample Converter", 1),
|
P25P2_SOFT_SYNC_DETECTOR("P25 Phase 2 Soft Sync Detector", 1),
|
||||||
SAMPLE_UNPACKED_INTERLEAVED_ITERATOR("Unpacked Interleaved Sample Iterator", 1),
|
SAMPLE_PACKED_CONVERTER("Packed Sample Converter", 2),
|
||||||
SAMPLE_UNPACKED_ITERATOR("Unpacked Sample Iterator", 1),
|
SAMPLE_UNPACKED_INTERLEAVED_ITERATOR("Unpacked Interleaved Sample Iterator", 2),
|
||||||
|
SAMPLE_UNPACKED_ITERATOR("Unpacked Sample Iterator", 2),
|
||||||
SQUELCHING_FM_DEMODULATOR("Squelching FM Demodulator", 1),
|
SQUELCHING_FM_DEMODULATOR("Squelching FM Demodulator", 1),
|
||||||
WINDOW("Window", 1);
|
WINDOW("Window", 1);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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();
|
ScalarUnpackedSampleConverter scalor = new ScalarUnpackedSampleConverter();
|
||||||
|
|
||||||
long count = 0;
|
long count = 0;
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
@@ -115,14 +115,14 @@ public class PackedSampleConverterCalibration extends Calibration
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count + (accumulator * 0);
|
return count + (long)(accumulator * 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector(ByteBuffer buffer)
|
private long testVector(ByteBuffer buffer)
|
||||||
{
|
{
|
||||||
VectorUnpackedSampleConverter vector = new VectorUnpackedSampleConverter();
|
VectorUnpackedSampleConverter vector = new VectorUnpackedSampleConverter();
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
@@ -132,6 +132,6 @@ public class PackedSampleConverterCalibration extends Calibration
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count + (accumulator * 0);
|
return count + (long)(accumulator * 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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)
|
private long calibrateScalar(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long calibrateVector64(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long calibrateVector128(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
for(int x = 0; x < ITERATION_DURATION_MS; x++)
|
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)
|
private long calibrateVector256(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long calibrateVector512(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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)
|
private long testScalar(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long testVector64(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long testVector128(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long testVector256(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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)
|
private long testVector512(short[] samples, short[] residualI, short[] residualQ)
|
||||||
{
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long accumulator = 0;
|
double accumulator = 0.0;
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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 mVectorDetector128 = new DMRSoftSyncDetectorVector128();
|
||||||
private final DMRSoftSyncDetector mVectorDetector256 = new DMRSoftSyncDetectorVector256();
|
private final DMRSoftSyncDetector mVectorDetector256 = new DMRSoftSyncDetectorVector256();
|
||||||
private final DMRSoftSyncDetector mVectorDetector512 = new DMRSoftSyncDetectorVector512();
|
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
|
* Constructs an instance
|
||||||
@@ -193,6 +196,7 @@ public class DMRSoftSyncCalibration extends Calibration
|
|||||||
private long testScalar(float[] samples)
|
private long testScalar(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
@@ -200,79 +204,88 @@ public class DMRSoftSyncCalibration extends Calibration
|
|||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mScalarDetector.process(sample);
|
accumulator += mScalarDetector.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector64(float[] samples)
|
private long testVector64(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector64.process(sample);
|
accumulator += mVectorDetector64.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector128(float[] samples)
|
private long testVector128(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector128.process(sample);
|
accumulator += mVectorDetector128.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector256(float[] samples)
|
private long testVector256(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector256.process(sample);
|
accumulator += mVectorDetector256.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector512(float[] samples)
|
private long testVector512(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector512.process(sample);
|
accumulator += mVectorDetector512.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ public class NXDNSoftSyncCalibration extends Calibration
|
|||||||
private final NXDNSoftSyncDetector mVectorDetector128 = new NXDNSoftSyncDetectorVector128();
|
private final NXDNSoftSyncDetector mVectorDetector128 = new NXDNSoftSyncDetectorVector128();
|
||||||
private final NXDNSoftSyncDetector mVectorDetector256 = new NXDNSoftSyncDetectorVector256();
|
private final NXDNSoftSyncDetector mVectorDetector256 = new NXDNSoftSyncDetectorVector256();
|
||||||
private final NXDNSoftSyncDetector mVectorDetector512 = new NXDNSoftSyncDetectorVector512();
|
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
|
* Constructs an instance
|
||||||
@@ -193,6 +196,7 @@ public class NXDNSoftSyncCalibration extends Calibration
|
|||||||
private long testScalar(float[] samples)
|
private long testScalar(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
@@ -200,79 +204,88 @@ public class NXDNSoftSyncCalibration extends Calibration
|
|||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mScalarDetector.process(sample);
|
accumulator += mScalarDetector.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector64(float[] samples)
|
private long testVector64(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector64.process(sample);
|
accumulator += mVectorDetector64.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector128(float[] samples)
|
private long testVector128(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector128.process(sample);
|
accumulator += mVectorDetector128.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector256(float[] samples)
|
private long testVector256(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector256.process(sample);
|
accumulator += mVectorDetector256.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector512(float[] samples)
|
private long testVector512(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector512.process(sample);
|
accumulator += mVectorDetector512.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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 mVectorDetector128 = new P25P1SoftSyncDetectorVector128();
|
||||||
private final P25P1SoftSyncDetector mVectorDetector256 = new P25P1SoftSyncDetectorVector256();
|
private final P25P1SoftSyncDetector mVectorDetector256 = new P25P1SoftSyncDetectorVector256();
|
||||||
private final P25P1SoftSyncDetector mVectorDetector512 = new P25P1SoftSyncDetectorVector512();
|
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
|
* Constructs an instance
|
||||||
@@ -193,6 +196,7 @@ public class P25P1SoftSyncCalibration extends Calibration
|
|||||||
private long testScalar(float[] samples)
|
private long testScalar(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
@@ -200,79 +204,88 @@ public class P25P1SoftSyncCalibration extends Calibration
|
|||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mScalarDetector.process(sample);
|
accumulator += mScalarDetector.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector64(float[] samples)
|
private long testVector64(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector64.process(sample);
|
accumulator += mVectorDetector64.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector128(float[] samples)
|
private long testVector128(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector128.process(sample);
|
accumulator += mVectorDetector128.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector256(float[] samples)
|
private long testVector256(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector256.process(sample);
|
accumulator += mVectorDetector256.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long testVector512(float[] samples)
|
private long testVector512(float[] samples)
|
||||||
{
|
{
|
||||||
long count = 0;
|
long count = 0;
|
||||||
|
float accumulator = 0.0f;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
|
||||||
{
|
{
|
||||||
for(float sample : samples)
|
for(float sample : samples)
|
||||||
{
|
{
|
||||||
mVectorDetector512.process(sample);
|
accumulator += mVectorDetector512.process(sample);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mScoreAccumulator += accumulator;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.filter.FilterFactory;
|
||||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
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;
|
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 SAMPLE_RATE = 24000;
|
||||||
private static final int SAMPLES_PER_SYMBOL = 5;
|
private static final int SAMPLES_PER_SYMBOL = 5;
|
||||||
@@ -122,7 +122,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
void testOffsetRemovalAndSymbolRecovery()
|
void testOffsetRemovalAndSymbolRecovery()
|
||||||
{
|
{
|
||||||
SignalGenerator generator = new SignalGenerator(300, 0.0);
|
SignalGenerator generator = new SignalGenerator(300, 0.0);
|
||||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||||
int symbolCount = 0;
|
int symbolCount = 0;
|
||||||
@@ -164,7 +164,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
void testAmbiguitySearch()
|
void testAmbiguitySearch()
|
||||||
{
|
{
|
||||||
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
||||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||||
boolean resolved = false;
|
boolean resolved = false;
|
||||||
@@ -197,7 +197,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
void testSearchHoldsAcrossSignalGap()
|
void testSearchHoldsAcrossSignalGap()
|
||||||
{
|
{
|
||||||
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
SignalGenerator generator = new SignalGenerator(-750, 0.1);
|
||||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||||
boolean resolved = false;
|
boolean resolved = false;
|
||||||
@@ -250,8 +250,8 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
{
|
{
|
||||||
SignalGenerator generatorUnverified = new SignalGenerator(-750, 0.1);
|
SignalGenerator generatorUnverified = new SignalGenerator(-750, 0.1);
|
||||||
SignalGenerator generatorVerified = new SignalGenerator(-750, 0.1);
|
SignalGenerator generatorVerified = new SignalGenerator(-750, 0.1);
|
||||||
P25P1DifferentialDemodulatorLSM unverified = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator unverified = new DQPSKDifferentialDemodulator();
|
||||||
P25P1DifferentialDemodulatorLSM verified = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator verified = new DQPSKDifferentialDemodulator();
|
||||||
unverified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
unverified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
verified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
verified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
Dibit[] completed = new Dibit[BUFFER_LENGTH];
|
||||||
@@ -287,7 +287,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
@Test
|
@Test
|
||||||
void testReliabilities()
|
void testReliabilities()
|
||||||
{
|
{
|
||||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
float[] amplitudes = new float[2 * BUFFER_LENGTH];
|
float[] amplitudes = new float[2 * BUFFER_LENGTH];
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ public class P25P1DifferentialDemodulatorLSMTest
|
|||||||
void testNoiseDoesNotMoveEstimate()
|
void testNoiseDoesNotMoveEstimate()
|
||||||
{
|
{
|
||||||
Random random = new Random(2);
|
Random random = new Random(2);
|
||||||
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
|
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
|
||||||
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
|
||||||
|
|
||||||
for(int buffer = 0; buffer < 2000; buffer++)
|
for(int buffer = 0; buffer < 2000; buffer++)
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.edac;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Random;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the Reed-Solomon errors-and-erasures decoder against the P25 RS(63,35,29) code.
|
||||||
|
*/
|
||||||
|
public class ReedSolomonErasureDecoderTest
|
||||||
|
{
|
||||||
|
private static final int N = 63;
|
||||||
|
private static final int K = 35;
|
||||||
|
private static final int PARITY = N - K;
|
||||||
|
private int[] randomCodeword(Random random)
|
||||||
|
{
|
||||||
|
int[] message = new int[K];
|
||||||
|
|
||||||
|
for(int i = 0; i < K; i++)
|
||||||
|
{
|
||||||
|
message[i] = random.nextInt(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReedSolomonTestEncoder.encode(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrupts the codeword with the specified number of errors and erasures at distinct random positions.
|
||||||
|
* @return erasure positions
|
||||||
|
*/
|
||||||
|
private int[] corrupt(int[] codeword, int errors, int erasures, Random random)
|
||||||
|
{
|
||||||
|
int[] positions = new int[N];
|
||||||
|
|
||||||
|
for(int i = 0; i < N; i++)
|
||||||
|
{
|
||||||
|
positions[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = N - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
int j = random.nextInt(i + 1);
|
||||||
|
int t = positions[i];
|
||||||
|
positions[i] = positions[j];
|
||||||
|
positions[j] = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < errors; i++)
|
||||||
|
{
|
||||||
|
codeword[positions[i]] ^= 1 + random.nextInt(63);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] erased = Arrays.copyOfRange(positions, errors, errors + erasures);
|
||||||
|
|
||||||
|
for(int position : erased)
|
||||||
|
{
|
||||||
|
codeword[position] = random.nextInt(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
return erased;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encoderProducesValidCodewords()
|
||||||
|
{
|
||||||
|
Random random = new Random(1);
|
||||||
|
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
|
||||||
|
|
||||||
|
for(int trial = 0; trial < 100; trial++)
|
||||||
|
{
|
||||||
|
int[] codeword = randomCodeword(random);
|
||||||
|
int[] output = new int[N];
|
||||||
|
assertFalse(reference.decode(codeword, output));
|
||||||
|
assertArrayEquals(codeword, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void correctsErrorsAndErasuresWithinCapacity()
|
||||||
|
{
|
||||||
|
Random random = new Random(2);
|
||||||
|
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||||
|
|
||||||
|
for(int erasures = 0; erasures <= PARITY; erasures++)
|
||||||
|
{
|
||||||
|
int maxErrors = (PARITY - erasures) / 2;
|
||||||
|
|
||||||
|
for(int errors = 0; errors <= maxErrors; errors++)
|
||||||
|
{
|
||||||
|
for(int trial = 0; trial < 20; trial++)
|
||||||
|
{
|
||||||
|
int[] codeword = randomCodeword(random);
|
||||||
|
int[] received = codeword.clone();
|
||||||
|
int[] erased = corrupt(received, errors, erasures, random);
|
||||||
|
int[] output = new int[N];
|
||||||
|
assertFalse(decoder.decode(received, output, erased, erased.length),
|
||||||
|
"Failed with errors [" + errors + "] erasures [" + erasures + "]");
|
||||||
|
assertArrayEquals(codeword, output, "Errors [" + errors + "] erasures [" + erasures + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void detectsMostDecodingFailuresBeyondCapacity()
|
||||||
|
{
|
||||||
|
Random random = new Random(3);
|
||||||
|
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||||
|
int miscorrections = 0;
|
||||||
|
int trials = 1000;
|
||||||
|
|
||||||
|
for(int trial = 0; trial < trials; trial++)
|
||||||
|
{
|
||||||
|
int[] codeword = randomCodeword(random);
|
||||||
|
int[] received = codeword.clone();
|
||||||
|
int erasures = 6;
|
||||||
|
int errors = (PARITY - erasures) / 2 + 3;
|
||||||
|
int[] erased = corrupt(received, errors, erasures, random);
|
||||||
|
int[] output = new int[N];
|
||||||
|
|
||||||
|
if(!decoder.decode(received, output, erased, erased.length))
|
||||||
|
{
|
||||||
|
//A miscorrection must still produce a valid (but incorrect) codeword
|
||||||
|
assertFalse(Arrays.equals(codeword, output));
|
||||||
|
int[] check = new int[N];
|
||||||
|
assertFalse(new ReedSolomon_63_35_29_P25().decode(output, check));
|
||||||
|
miscorrections++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(miscorrections < trials / 100, "Miscorrections: " + miscorrections);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void matchesErrorsOnlyDecoder()
|
||||||
|
{
|
||||||
|
Random random = new Random(4);
|
||||||
|
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
|
||||||
|
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
|
||||||
|
|
||||||
|
for(int trial = 0; trial < 500; trial++)
|
||||||
|
{
|
||||||
|
int[] received = randomCodeword(random);
|
||||||
|
corrupt(received, random.nextInt(18), 0, random);
|
||||||
|
int[] output = new int[N];
|
||||||
|
int[] referenceOutput = new int[N];
|
||||||
|
boolean failed = decoder.decode(received, output, new int[0], 0);
|
||||||
|
boolean referenceFailed = reference.decode(received, referenceOutput);
|
||||||
|
assertEquals(referenceFailed, failed);
|
||||||
|
|
||||||
|
if(!failed)
|
||||||
|
{
|
||||||
|
assertArrayEquals(referenceOutput, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.edac;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Systematic encoder for the P25 RS(63,35,29) code (and codes shortened from it) for unit tests. Parity symbols are
|
||||||
|
* at indices 0-27 and message symbols at indices 28-62, with generator polynomial roots alpha^1 through alpha^28.
|
||||||
|
*/
|
||||||
|
public class ReedSolomonTestEncoder
|
||||||
|
{
|
||||||
|
private static final int N = 63;
|
||||||
|
private static final int K = 35;
|
||||||
|
private static final int PARITY = N - K;
|
||||||
|
private static final int[] EXP = new int[63];
|
||||||
|
private static final int[] LOG = new int[64];
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
int value = 1;
|
||||||
|
|
||||||
|
for(int i = 0; i < 63; i++)
|
||||||
|
{
|
||||||
|
EXP[i] = value;
|
||||||
|
LOG[value] = i;
|
||||||
|
value <<= 1;
|
||||||
|
|
||||||
|
if(value > 63)
|
||||||
|
{
|
||||||
|
value = (value & 63) ^ 3; //x^6 = x + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int multiply(int a, int b)
|
||||||
|
{
|
||||||
|
return (a == 0 || b == 0) ? 0 : EXP[(LOG[a] + LOG[b]) % 63];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the message symbols.
|
||||||
|
* @param message symbols (35), where trailing zero symbols represent a shortened code
|
||||||
|
* @return codeword (63 symbols)
|
||||||
|
*/
|
||||||
|
public static int[] encode(int[] message)
|
||||||
|
{
|
||||||
|
int[] generator = new int[PARITY + 1];
|
||||||
|
generator[0] = 1;
|
||||||
|
|
||||||
|
for(int i = 1; i <= PARITY; i++)
|
||||||
|
{
|
||||||
|
int root = EXP[i % 63];
|
||||||
|
|
||||||
|
for(int j = i; j > 0; j--)
|
||||||
|
{
|
||||||
|
generator[j] = generator[j - 1] ^ multiply(generator[j], root);
|
||||||
|
}
|
||||||
|
|
||||||
|
generator[0] = multiply(generator[0], root);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remainder of x^28 * m(x) divided by g(x)
|
||||||
|
int[] remainder = new int[N];
|
||||||
|
System.arraycopy(message, 0, remainder, PARITY, K);
|
||||||
|
|
||||||
|
for(int i = N - 1; i >= PARITY; i--)
|
||||||
|
{
|
||||||
|
int coefficient = remainder[i];
|
||||||
|
|
||||||
|
if(coefficient != 0)
|
||||||
|
{
|
||||||
|
for(int j = 0; j <= PARITY; j++)
|
||||||
|
{
|
||||||
|
remainder[i - PARITY + j] ^= multiply(generator[j], coefficient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] codeword = new int[N];
|
||||||
|
System.arraycopy(remainder, 0, codeword, 0, PARITY);
|
||||||
|
System.arraycopy(message, 0, codeword, PARITY, K);
|
||||||
|
return codeword;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.edac.bch.BCH_63_16_23_P25;
|
|||||||
import io.github.dsheirer.edac.trellis.P25_1_2_Node;
|
import io.github.dsheirer.edac.trellis.P25_1_2_Node;
|
||||||
import io.github.dsheirer.message.IMessage;
|
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.message.tsbk.TSBKMessage;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SyncDetector;
|
||||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
@@ -252,7 +253,7 @@ public class P25P1DecoderLSMTest
|
|||||||
private static List<Dibit> getFrame(BitSet tsbk)
|
private static List<Dibit> getFrame(BitSet tsbk)
|
||||||
{
|
{
|
||||||
List<Dibit> data = new ArrayList<>();
|
List<Dibit> data = new ArrayList<>();
|
||||||
addBits(data, 0x5575F5FF77FFL, 48);
|
addBits(data, P25P1SyncDetector.SYNC_PATTERN, 48);
|
||||||
addBits(data, BCH_63_16_23_P25.encode(NAC, TSBK_DUID) << 1, 64); //64th (parity) bit is zero
|
addBits(data, BCH_63_16_23_P25.encode(NAC, TSBK_DUID) << 1, 64); //64th (parity) bit is zero
|
||||||
data.addAll(encodeTSBK(tsbk));
|
data.addAll(encodeTSBK(tsbk));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase1.sync;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit tests for the P25 Phase 1 soft sync detector scalar and SIMD vector implementations.
|
||||||
|
*/
|
||||||
|
public class P25P1SoftSyncDetectorTest
|
||||||
|
{
|
||||||
|
private static final float IDEAL_SCORE = (float)(24 * Math.pow(3.0 * Math.PI / 4.0, 2));
|
||||||
|
|
||||||
|
private static P25P1SoftSyncDetector[] getDetectors()
|
||||||
|
{
|
||||||
|
return new P25P1SoftSyncDetector[]{new P25P1SoftSyncDetectorScalar(), new P25P1SoftSyncDetectorVector64(),
|
||||||
|
new P25P1SoftSyncDetectorVector128(), new P25P1SoftSyncDetectorVector256(),
|
||||||
|
new P25P1SoftSyncDetectorVector512()};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void detectsSyncPattern()
|
||||||
|
{
|
||||||
|
for(P25P1SoftSyncDetector detector : getDetectors())
|
||||||
|
{
|
||||||
|
float[] pattern = detector.getPatternSymbols();
|
||||||
|
float score = 0;
|
||||||
|
|
||||||
|
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
|
||||||
|
for(int repeat = 0; repeat < 3; repeat++)
|
||||||
|
{
|
||||||
|
for(float symbol : pattern)
|
||||||
|
{
|
||||||
|
score = detector.process(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vectorImplementationsMatchScalar()
|
||||||
|
{
|
||||||
|
P25P1SoftSyncDetector[] detectors = getDetectors();
|
||||||
|
Random random = new Random(1);
|
||||||
|
|
||||||
|
for(int x = 0; x < 1000; x++)
|
||||||
|
{
|
||||||
|
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
|
||||||
|
float expected = detectors[0].process(symbol);
|
||||||
|
|
||||||
|
for(int d = 1; d < detectors.length; d++)
|
||||||
|
{
|
||||||
|
assertEquals(expected, detectors[d].process(symbol), 0.001,
|
||||||
|
detectors[d].getClass().getSimpleName() + " at symbol " + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,548 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.FragmentedIntField;
|
||||||
|
import io.github.dsheirer.bits.IntField;
|
||||||
|
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||||
|
import io.github.dsheirer.edac.CRCP25;
|
||||||
|
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
|
||||||
|
import io.github.dsheirer.message.IMessage;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetector;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.timeslot.FacchTimeslot;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.timeslot.ScramblingSequence;
|
||||||
|
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-end JUnit tests for the P25 Phase 2 HDQPSK decoder: a synthetic outbound (FDMA) channel carrying super frame
|
||||||
|
* fragments with I-ISCH, S-ISCH (sync) and four scrambled FACCH timeslots is generated and decoded, and each valid
|
||||||
|
* decoded MAC message is checked against the transmitted MAC messages.
|
||||||
|
*/
|
||||||
|
public class P25P2DecoderHDQPSKTest
|
||||||
|
{
|
||||||
|
private static final double SAMPLE_RATE = 48000.0;
|
||||||
|
private static final double SYMBOL_RATE = 6000.0;
|
||||||
|
private static final int FRAGMENT_COUNT = 60;
|
||||||
|
private static final int TRAILING_FRAGMENT_COUNT = 2;
|
||||||
|
private static final ScrambleParameters SCRAMBLE_PARAMETERS = new ScrambleParameters(0xBEE00, 0x3BB, 0x3B2);
|
||||||
|
private static final long ISCH_CODE_WORD_OFFSET = 0x184229d461L;
|
||||||
|
private static final int[] DUID_BITS = {0, 1, 74, 75, 244, 245, 318, 319};
|
||||||
|
//FACCH MAC PDU: 156 bits, with the CRC-12 checksum in the final 12 bits
|
||||||
|
private static final int MAC_LENGTH = 156;
|
||||||
|
private static final int MAC_CRC_START = 144;
|
||||||
|
private static final int MAC_PDU_TYPE_IDLE = 3;
|
||||||
|
//Raised cosine transmit pulse
|
||||||
|
private static final double ROLLOFF = 0.2;
|
||||||
|
private static final int PULSE_SPAN_SYMBOLS = 8;
|
||||||
|
private static final int BUFFER_SIZE = 2048;
|
||||||
|
private static final Dibit[] DIBITS = {Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3};
|
||||||
|
private static final int MESSAGES_PER_FRAGMENT = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: carrier offsets beyond the fourth power offset estimator range are resolved by the offset ambiguity search,
|
||||||
|
* after which every FACCH MAC message decodes. A fragment framed while the search steps to the correct offset range
|
||||||
|
* can produce an invalid (CRC failure) MAC message.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testOffsetAmbiguitySearch()
|
||||||
|
{
|
||||||
|
for(double carrierOffset : new double[]{1000.0, -1000.0, 1200.0, -1200.0})
|
||||||
|
{
|
||||||
|
Result result = decode(carrierOffset, -100.0, Double.NaN, 3, false);
|
||||||
|
assertTrue(result.invalid() <= 1, "Invalid messages at " + carrierOffset + " Hz: " + result.invalid());
|
||||||
|
assertTrue(result.first() <= 12, "Acquisition at " + carrierOffset + " Hz: " + result.first() + " fragments");
|
||||||
|
assertEquals(0, result.missed(), "Missed messages after acquisition at " + carrierOffset + " Hz");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: with additive white gaussian noise at 12 dB in-band (+/- 3.6 kHz) carrier to noise, nearly all FACCH MAC
|
||||||
|
* messages decode and every valid MAC message matches a transmitted MAC message.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNoise()
|
||||||
|
{
|
||||||
|
Result result = decode(200.0, 50.0, 12.0, 5, false);
|
||||||
|
assertTrue(result.valid() >= FRAGMENT_COUNT * MESSAGES_PER_FRAGMENT * 0.9, "Valid messages: " + result.valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates and decodes the synthetic signal.
|
||||||
|
* @param carrierOffset in Hertz
|
||||||
|
* @param symbolRateError in parts per million
|
||||||
|
* @param carrierToNoise in dB, in-band (+/- 3.6 kHz), or NaN for no noise
|
||||||
|
* @param seed for the random MAC payloads and noise
|
||||||
|
* @param corruptIsch to corrupt both I-ISCH in the second and third fragment of each superframe
|
||||||
|
* @return decoded message counts (valid messages for the checked fragments), the index of the first fragment with a decoded MAC message (the fragment count
|
||||||
|
* when none decode), the count of MAC messages that didn't decode after that fragment, and the count of valid MAC
|
||||||
|
* messages decoded from the replayed dibit stream.
|
||||||
|
*/
|
||||||
|
private static Result decode(double carrierOffset, double symbolRateError, double carrierToNoise, long seed,
|
||||||
|
boolean corruptIsch)
|
||||||
|
{
|
||||||
|
Random random = new Random(seed);
|
||||||
|
ScramblingSequence scramblingSequence = new ScramblingSequence();
|
||||||
|
scramblingSequence.update(SCRAMBLE_PARAMETERS);
|
||||||
|
List<BinaryMessage> macs = new ArrayList<>();
|
||||||
|
Set<BinaryMessage> transmitted = new HashSet<>();
|
||||||
|
List<Dibit> dibits = new ArrayList<>();
|
||||||
|
|
||||||
|
for(int fragment = 0; fragment < FRAGMENT_COUNT + TRAILING_FRAGMENT_COUNT; fragment++)
|
||||||
|
{
|
||||||
|
List<BinaryMessage> fragmentMacs = new ArrayList<>();
|
||||||
|
|
||||||
|
for(int x = 0; x < MESSAGES_PER_FRAGMENT; x++)
|
||||||
|
{
|
||||||
|
fragmentMacs.add(getMacMessage(random));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Trailing fragments aren't checked since the final fragment isn't dispatched until its trailing symbols are
|
||||||
|
//processed, but they are included in the transmitted messages for the CRC false positive check.
|
||||||
|
if(fragment < FRAGMENT_COUNT)
|
||||||
|
{
|
||||||
|
macs.addAll(fragmentMacs);
|
||||||
|
}
|
||||||
|
|
||||||
|
transmitted.addAll(fragmentMacs);
|
||||||
|
|
||||||
|
boolean corrupt = corruptIsch && (fragment % 3) != 0;
|
||||||
|
dibits.addAll(getFragment(fragment, fragmentMacs, scramblingSequence, corrupt, random));
|
||||||
|
}
|
||||||
|
|
||||||
|
float[][] samples = modulate(dibits, carrierOffset, symbolRateError, carrierToNoise, random);
|
||||||
|
|
||||||
|
List<MacMessage> received = new ArrayList<>();
|
||||||
|
List<ByteBuffer> buffers = new ArrayList<>();
|
||||||
|
DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2();
|
||||||
|
config.setScrambleParameters(SCRAMBLE_PARAMETERS);
|
||||||
|
P25P2DecoderHDQPSK decoder = new P25P2DecoderHDQPSK(config);
|
||||||
|
decoder.setMessageListener((IMessage message) -> {
|
||||||
|
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH())
|
||||||
|
{
|
||||||
|
received.add(mac);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
decoder.setBufferListener(buffer -> {
|
||||||
|
ByteBuffer copy = ByteBuffer.allocate(buffer.capacity());
|
||||||
|
copy.put(buffer.array());
|
||||||
|
buffers.add(copy);
|
||||||
|
});
|
||||||
|
decoder.start();
|
||||||
|
decoder.setSampleRate(SAMPLE_RATE);
|
||||||
|
|
||||||
|
for(int offset = 0; offset < samples[0].length; offset += BUFFER_SIZE)
|
||||||
|
{
|
||||||
|
int length = Math.min(BUFFER_SIZE, samples[0].length - offset);
|
||||||
|
float[] i = new float[length];
|
||||||
|
float[] q = new float[length];
|
||||||
|
System.arraycopy(samples[0], offset, i, 0, length);
|
||||||
|
System.arraycopy(samples[1], offset, q, 0, length);
|
||||||
|
decoder.receive(new ComplexSamples(i, q, 0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder.stop();
|
||||||
|
|
||||||
|
int invalid = 0;
|
||||||
|
Set<BinaryMessage> decoded = new HashSet<>();
|
||||||
|
|
||||||
|
for(MacMessage mac : received)
|
||||||
|
{
|
||||||
|
if(mac.isValid())
|
||||||
|
{
|
||||||
|
BinaryMessage bits = getBits(mac.getMessage());
|
||||||
|
//A valid MAC message that doesn't match a transmitted MAC message is a CRC false positive
|
||||||
|
assertTrue(transmitted.contains(bits), "Decoded MAC message was not transmitted");
|
||||||
|
assertTrue(decoded.add(bits), "Decoded MAC message was duplicated");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
invalid++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int valid = 0;
|
||||||
|
|
||||||
|
int first = FRAGMENT_COUNT;
|
||||||
|
int missed = 0;
|
||||||
|
|
||||||
|
for(int x = 0; x < macs.size(); x++)
|
||||||
|
{
|
||||||
|
int fragment = x / MESSAGES_PER_FRAGMENT;
|
||||||
|
|
||||||
|
if(decoded.contains(macs.get(x)))
|
||||||
|
{
|
||||||
|
valid++;
|
||||||
|
first = Math.min(first, fragment);
|
||||||
|
}
|
||||||
|
else if(first < fragment)
|
||||||
|
{
|
||||||
|
missed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result(valid, invalid, first, missed, replay(buffers, new HashSet<>(macs)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replays the demodulated dibit stream through a message framer with hard sync detection.
|
||||||
|
* @return count of valid MAC messages that match a checked (transmitted) MAC message.
|
||||||
|
*/
|
||||||
|
private static int replay(List<ByteBuffer> buffers, Set<BinaryMessage> transmitted)
|
||||||
|
{
|
||||||
|
Set<BinaryMessage> decoded = new HashSet<>();
|
||||||
|
P25P2MessageFramer framer = new P25P2MessageFramer();
|
||||||
|
framer.setScrambleParameters(SCRAMBLE_PARAMETERS);
|
||||||
|
P25P2MessageProcessor processor = new P25P2MessageProcessor();
|
||||||
|
processor.setMessageListener((IMessage message) -> {
|
||||||
|
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH() && mac.isValid())
|
||||||
|
{
|
||||||
|
BinaryMessage bits = getBits(mac.getMessage());
|
||||||
|
|
||||||
|
if(transmitted.contains(bits))
|
||||||
|
{
|
||||||
|
decoded.add(bits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
framer.setListener(processor);
|
||||||
|
framer.start();
|
||||||
|
|
||||||
|
for(ByteBuffer buffer : buffers)
|
||||||
|
{
|
||||||
|
framer.receive(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the MAC message bits for comparison against the transmitted MAC messages.
|
||||||
|
*/
|
||||||
|
private static BinaryMessage getBits(CorrectedBinaryMessage message)
|
||||||
|
{
|
||||||
|
BinaryMessage bits = new BinaryMessage(MAC_LENGTH);
|
||||||
|
|
||||||
|
for(int x = 0; x < MAC_LENGTH; x++)
|
||||||
|
{
|
||||||
|
if(message.get(x))
|
||||||
|
{
|
||||||
|
bits.set(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a random MAC IDLE PDU with a NULL INFORMATION MAC structure, random payload bits and the CRC-12 checksum.
|
||||||
|
*/
|
||||||
|
private static BinaryMessage getMacMessage(Random random)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage message = new CorrectedBinaryMessage(MAC_LENGTH);
|
||||||
|
message.load(0, 3, MAC_PDU_TYPE_IDLE);
|
||||||
|
//Opcode (bits 8-15) is zero for the NULL INFORMATION MAC structure
|
||||||
|
|
||||||
|
for(int x = 16; x < MAC_CRC_START; x++)
|
||||||
|
{
|
||||||
|
if(random.nextBoolean())
|
||||||
|
{
|
||||||
|
message.set(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//The checksum isn't included in the CRC calculation, so exactly one checksum value passes the CRC check
|
||||||
|
for(int checksum = 0; checksum < 4096; checksum++)
|
||||||
|
{
|
||||||
|
message.load(MAC_CRC_START, 12, checksum);
|
||||||
|
|
||||||
|
if(CRCP25.crc12_FACCH(message))
|
||||||
|
{
|
||||||
|
return getBits(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Unable to calculate the CRC-12 checksum");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the 720 dibit super frame fragment: I-ISCH 1, timeslot A, I-ISCH 2, timeslot B, S-ISCH (sync),
|
||||||
|
* timeslot C, S-ISCH (sync), timeslot D.
|
||||||
|
* @param fragment index, which determines the ISCH sequence (fragment 1-3 of the superframe)
|
||||||
|
* @param macs for timeslots A-D
|
||||||
|
* @param scramblingSequence to scramble the timeslots
|
||||||
|
* @param corruptIsch to corrupt both I-ISCH beyond the error correction capability
|
||||||
|
* @param random for selecting the corrupted bits
|
||||||
|
*/
|
||||||
|
private static List<Dibit> getFragment(int fragment, List<BinaryMessage> macs, ScramblingSequence scramblingSequence,
|
||||||
|
boolean corruptIsch, Random random)
|
||||||
|
{
|
||||||
|
int ischSequence = fragment % 3;
|
||||||
|
int superframeSequence = (fragment / 3) % 4;
|
||||||
|
List<Dibit> dibits = new ArrayList<>();
|
||||||
|
|
||||||
|
for(int position = 0; position < 4; position++)
|
||||||
|
{
|
||||||
|
switch(position)
|
||||||
|
{
|
||||||
|
case 0, 1 -> addBits(dibits, getIISCH(position, ischSequence, superframeSequence, corruptIsch, random), 40);
|
||||||
|
default -> addBits(dibits, P25P2SoftSyncDetector.SYNC_PATTERN, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryMessage sequence = scramblingSequence.getTimeslotSequence(ischSequence * 4 + position);
|
||||||
|
BinaryMessage timeslot = getTimeslot(macs.get(position), sequence);
|
||||||
|
|
||||||
|
for(int x = 0; x < 320; x += 2)
|
||||||
|
{
|
||||||
|
dibits.add(DIBITS[(timeslot.get(x) ? 2 : 0) + (timeslot.get(x + 1) ? 1 : 0)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dibits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the 40-bit I-ISCH codeword.
|
||||||
|
* @param channel 0 (timeslot 1) or 1 (timeslot 2)
|
||||||
|
* @param ischSequence 0-2 (fragment 1-3 of the superframe)
|
||||||
|
* @param superframeSequence 0-3
|
||||||
|
* @param corrupt to flip 12 codeword bits, which exceeds the 7 bit error correction capability
|
||||||
|
* @param random for selecting the corrupted bits
|
||||||
|
*/
|
||||||
|
private static long getIISCH(int channel, int ischSequence, int superframeSequence, boolean corrupt, Random random)
|
||||||
|
{
|
||||||
|
BinaryMessage message = new BinaryMessage(9);
|
||||||
|
message.load(2, 2, channel);
|
||||||
|
message.load(4, 2, ischSequence);
|
||||||
|
message.load(7, 2, superframeSequence);
|
||||||
|
long codeword = ISCHDecoder.getCodeWord(message) ^ ISCH_CODE_WORD_OFFSET;
|
||||||
|
|
||||||
|
if(corrupt)
|
||||||
|
{
|
||||||
|
Set<Integer> bits = new HashSet<>();
|
||||||
|
|
||||||
|
while(bits.size() < 12)
|
||||||
|
{
|
||||||
|
bits.add(random.nextInt(40));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int bit : bits)
|
||||||
|
{
|
||||||
|
codeword ^= 1L << bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the scrambled 320-bit FACCH timeslot: RS(63,35,29) encodes the 26 MAC message hexbits, loads the message
|
||||||
|
* and transmitted (19) parity hexbits into the FACCH timeslot fields, scrambles the timeslot, and sets the
|
||||||
|
* (unscrambled) data unit ID.
|
||||||
|
*/
|
||||||
|
private static BinaryMessage getTimeslot(BinaryMessage mac, BinaryMessage scramblingSequence)
|
||||||
|
{
|
||||||
|
//Message hexbit h (INFO_h+1) is codeword index 53 - h, and parity hexbit p (PARITY_p) is codeword index 28 - p
|
||||||
|
int[] message = new int[35];
|
||||||
|
|
||||||
|
for(int h = 0; h < 26; h++)
|
||||||
|
{
|
||||||
|
message[25 - h] = mac.getInt(h * 6, h * 6 + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] codeword = ReedSolomonTestEncoder.encode(message);
|
||||||
|
BinaryMessage timeslot = new BinaryMessage(320);
|
||||||
|
|
||||||
|
for(int h = 1; h <= 26; h++)
|
||||||
|
{
|
||||||
|
load(timeslot, getFacchField("INFO_" + h), codeword[54 - h]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int p = 1; p <= 19; p++)
|
||||||
|
{
|
||||||
|
load(timeslot, getFacchField("PARITY_" + p), codeword[28 - p]);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeslot.xor(scramblingSequence);
|
||||||
|
|
||||||
|
int duid = DataUnitID.SCRAMBLED_FACCH.getValueWithParity();
|
||||||
|
|
||||||
|
for(int x = 0; x < DUID_BITS.length; x++)
|
||||||
|
{
|
||||||
|
if(((duid >> (7 - x)) & 1) == 1)
|
||||||
|
{
|
||||||
|
timeslot.set(DUID_BITS[x]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timeslot.clear(DUID_BITS[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeslot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accesses the FACCH timeslot hexbit field (IntField or FragmentedIntField) by name.
|
||||||
|
*/
|
||||||
|
private static Object getFacchField(String name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Field field = FacchTimeslot.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(null);
|
||||||
|
}
|
||||||
|
catch(ReflectiveOperationException e)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("FACCH timeslot field " + name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the 6-bit value into the field bits, most significant bit first.
|
||||||
|
*/
|
||||||
|
private static void load(BinaryMessage message, Object field, int value)
|
||||||
|
{
|
||||||
|
int[] indices;
|
||||||
|
|
||||||
|
if(field instanceof IntField intField)
|
||||||
|
{
|
||||||
|
indices = new int[intField.width()];
|
||||||
|
|
||||||
|
for(int x = 0; x < indices.length; x++)
|
||||||
|
{
|
||||||
|
indices[x] = intField.start() + x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indices = ((FragmentedIntField)field).indices();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int x = 0; x < indices.length; x++)
|
||||||
|
{
|
||||||
|
if(((value >> (indices.length - 1 - x)) & 1) == 1)
|
||||||
|
{
|
||||||
|
message.set(indices[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modulates the dibits as HDQPSK: differential QPSK with raised cosine pulse shaping.
|
||||||
|
* @return i and q sample arrays
|
||||||
|
*/
|
||||||
|
private static float[][] modulate(List<Dibit> dibits, double carrierOffset, double symbolRateError,
|
||||||
|
double carrierToNoise, Random random)
|
||||||
|
{
|
||||||
|
double[] symbolI = new double[dibits.size()];
|
||||||
|
double[] symbolQ = new double[dibits.size()];
|
||||||
|
double phase = 0.0;
|
||||||
|
|
||||||
|
for(int x = 0; x < dibits.size(); x++)
|
||||||
|
{
|
||||||
|
phase += dibits.get(x).getIdealPhase();
|
||||||
|
symbolI[x] = Math.cos(phase);
|
||||||
|
symbolQ[x] = Math.sin(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
double samplesPerSymbol = SAMPLE_RATE / (SYMBOL_RATE * (1.0 + symbolRateError * 1e-6));
|
||||||
|
//The decimation filter requires an even sample count
|
||||||
|
int length = (int)((dibits.size() + PULSE_SPAN_SYMBOLS) * samplesPerSymbol) / 2 * 2;
|
||||||
|
float[] i = new float[length];
|
||||||
|
float[] q = new float[length];
|
||||||
|
|
||||||
|
//Unit power symbols: the in-band noise variance is the in-band fraction of the total noise variance
|
||||||
|
double noise = Double.isNaN(carrierToNoise) ? 0.0 :
|
||||||
|
Math.sqrt(Math.pow(10.0, -carrierToNoise / 10.0) * SAMPLE_RATE / 7200.0 / 2.0);
|
||||||
|
|
||||||
|
for(int n = 0; n < length; n++)
|
||||||
|
{
|
||||||
|
double t = n / samplesPerSymbol - PULSE_SPAN_SYMBOLS / 2.0; //Symbol periods
|
||||||
|
double sumI = 0.0;
|
||||||
|
double sumQ = 0.0;
|
||||||
|
|
||||||
|
for(int k = (int)Math.ceil(t - PULSE_SPAN_SYMBOLS / 2.0); k <= t + PULSE_SPAN_SYMBOLS / 2.0; k++)
|
||||||
|
{
|
||||||
|
if(k >= 0 && k < dibits.size())
|
||||||
|
{
|
||||||
|
double pulse = getRaisedCosine(t - k);
|
||||||
|
sumI += symbolI[k] * pulse;
|
||||||
|
sumQ += symbolQ[k] * pulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double carrier = 2.0 * Math.PI * carrierOffset * n / SAMPLE_RATE;
|
||||||
|
double cos = Math.cos(carrier);
|
||||||
|
double sin = Math.sin(carrier);
|
||||||
|
i[n] = (float)(sumI * cos - sumQ * sin + noise * random.nextGaussian());
|
||||||
|
q[n] = (float)(sumI * sin + sumQ * cos + noise * random.nextGaussian());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new float[][]{i, q};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raised cosine pulse value
|
||||||
|
* @param t time in symbol periods
|
||||||
|
*/
|
||||||
|
private static double getRaisedCosine(double t)
|
||||||
|
{
|
||||||
|
double sinc = Math.abs(t) < 1e-9 ? 1.0 : Math.sin(Math.PI * t) / (Math.PI * t);
|
||||||
|
double denominator = 1.0 - Math.pow(2.0 * ROLLOFF * t, 2.0);
|
||||||
|
|
||||||
|
if(Math.abs(denominator) < 1e-9)
|
||||||
|
{
|
||||||
|
return Math.PI / 4.0 * sinc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sinc * Math.cos(Math.PI * ROLLOFF * t) / denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the most significant bits first, as dibits.
|
||||||
|
*/
|
||||||
|
private static void addBits(List<Dibit> dibits, long value, int bitCount)
|
||||||
|
{
|
||||||
|
for(int bit = bitCount - 2; bit >= 0; bit -= 2)
|
||||||
|
{
|
||||||
|
dibits.add(DIBITS[(int)((value >> bit) & 3)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Result(int valid, int invalid, int first, int missed, int replayValid) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.enumeration;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit tests for the ISCH (super frame fragment) sequence.
|
||||||
|
*/
|
||||||
|
public class ISCHSequenceTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void advanceCyclesThroughFragments()
|
||||||
|
{
|
||||||
|
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_1.advance(1));
|
||||||
|
assertEquals(ISCHSequence.ISCH_3, ISCHSequence.ISCH_1.advance(2));
|
||||||
|
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_1.advance(3));
|
||||||
|
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_3.advance(1));
|
||||||
|
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_3.advance(11));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void advanceOfNonFragmentSequenceIsUnknown()
|
||||||
|
{
|
||||||
|
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.RESERVED_4.advance(1));
|
||||||
|
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.UNKNOWN.advance(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.message;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
|
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
|
||||||
|
import java.util.Random;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit tests for the P25 Phase 2 Encryption Synchronization Sequence (ESS) RS(44,16,29) decoding.
|
||||||
|
*/
|
||||||
|
public class EncryptionSynchronizationSequenceProcessorTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates the RS codeword for the 16 ESS information symbols, where codeword index 43 is the first information
|
||||||
|
* symbol, indices 28-43 are information symbols and indices 0-27 are parity symbols.
|
||||||
|
*/
|
||||||
|
private static int[] encode(int[] information)
|
||||||
|
{
|
||||||
|
int[] message = new int[35];
|
||||||
|
|
||||||
|
for(int x = 0; x < 16; x++)
|
||||||
|
{
|
||||||
|
message[15 - x] = information[x];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReedSolomonTestEncoder.encode(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESS-A fragment (168 bits) containing the 28 parity symbols, where hexbit x is codeword index 27 - x.
|
||||||
|
*/
|
||||||
|
private static BinaryMessage getEssA(int[] codeword)
|
||||||
|
{
|
||||||
|
BinaryMessage essA = new BinaryMessage(168);
|
||||||
|
|
||||||
|
for(int x = 0; x < 28; x++)
|
||||||
|
{
|
||||||
|
essA.load(x * 6, 6, codeword[27 - x]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return essA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESS-B fragment (24 bits) containing 4 information symbols, where fragment 1 is codeword indices 40-43 and
|
||||||
|
* fragment 4 is codeword indices 28-31, and hexbit x is codeword index (base + 3 - x).
|
||||||
|
*/
|
||||||
|
private static BinaryMessage getEssB(int[] codeword, int fragment)
|
||||||
|
{
|
||||||
|
int base = 28 + (4 - fragment) * 4;
|
||||||
|
BinaryMessage essB = new BinaryMessage(24);
|
||||||
|
|
||||||
|
for(int x = 0; x < 4; x++)
|
||||||
|
{
|
||||||
|
essB.load(x * 6, 6, codeword[base + 3 - x]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return essB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected decoded 96-bit ESS message.
|
||||||
|
*/
|
||||||
|
private static String getExpected(int[] information)
|
||||||
|
{
|
||||||
|
BinaryMessage expected = new BinaryMessage(96);
|
||||||
|
|
||||||
|
for(int x = 0; x < 16; x++)
|
||||||
|
{
|
||||||
|
expected.load(x * 6, 6, information[x]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expected.toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrupts the specified number of distinct ESS-A (parity) symbols.
|
||||||
|
*/
|
||||||
|
private static void corruptParity(int[] codeword, int errors, Random random)
|
||||||
|
{
|
||||||
|
boolean[] corrupted = new boolean[28];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while(count < errors)
|
||||||
|
{
|
||||||
|
int index = random.nextInt(28);
|
||||||
|
|
||||||
|
if(!corrupted[index])
|
||||||
|
{
|
||||||
|
corrupted[index] = true;
|
||||||
|
codeword[index] ^= 1 + random.nextInt(63);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] getEncryptedInformation(Random random)
|
||||||
|
{
|
||||||
|
int[] information = new int[16];
|
||||||
|
|
||||||
|
for(int x = 0; x < 16; x++)
|
||||||
|
{
|
||||||
|
information[x] = random.nextInt(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
return information;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodesCompleteSequenceWithErrors()
|
||||||
|
{
|
||||||
|
Random random = new Random(1);
|
||||||
|
|
||||||
|
for(int trial = 0; trial < 100; trial++)
|
||||||
|
{
|
||||||
|
int[] information = getEncryptedInformation(random);
|
||||||
|
int[] codeword = encode(information);
|
||||||
|
int[] received = codeword.clone();
|
||||||
|
corruptParity(received, 14, random);
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||||
|
processor.setESSA(getEssA(received));
|
||||||
|
processor.setESSB1(getEssB(received, 1));
|
||||||
|
processor.setESSB2(getEssB(received, 2));
|
||||||
|
processor.setESSB3(getEssB(received, 3));
|
||||||
|
processor.setESSB4(getEssB(received, 4));
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||||
|
assertNotNull(sequence);
|
||||||
|
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypted sequence (random message indicator) with 3 missing ESS-B fragments and 4 parity symbol errors. The
|
||||||
|
* missing fragments can't be decoded as zero valued symbols (12 + 4 errors exceeds the 14 symbol error capacity),
|
||||||
|
* but can be decoded as erasures (12 erasures + 4 errors).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void decodesEncryptedSequenceWithMissingFragmentsAsErasures()
|
||||||
|
{
|
||||||
|
Random random = new Random(2);
|
||||||
|
|
||||||
|
for(int trial = 0; trial < 100; trial++)
|
||||||
|
{
|
||||||
|
int[] information = getEncryptedInformation(random);
|
||||||
|
int[] codeword = encode(information);
|
||||||
|
int[] received = codeword.clone();
|
||||||
|
corruptParity(received, 4, random);
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||||
|
processor.setESSA(getEssA(received));
|
||||||
|
processor.setESSB1(getEssB(received, 1));
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||||
|
assertNotNull(sequence, "Trial " + trial);
|
||||||
|
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unencrypted sequence (algorithm 0x80 with zero key ID and message indicator) with 3 missing ESS-B fragments and
|
||||||
|
* 9 parity symbol errors. The missing fragments decode as zero valued symbols (9 errors), but can't be decoded
|
||||||
|
* as erasures (12 erasures + 9 errors exceeds the capacity).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void decodesUnencryptedSequenceWithMissingFragmentsAsZeros()
|
||||||
|
{
|
||||||
|
Random random = new Random(3);
|
||||||
|
int[] information = new int[16];
|
||||||
|
information[0] = 0x20; //Algorithm 0x80 (unencrypted) in the first 8 bits
|
||||||
|
|
||||||
|
for(int trial = 0; trial < 100; trial++)
|
||||||
|
{
|
||||||
|
int[] codeword = encode(information);
|
||||||
|
int[] received = codeword.clone();
|
||||||
|
corruptParity(received, 9, random);
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
|
||||||
|
processor.setESSA(getEssA(received));
|
||||||
|
processor.setESSB1(getEssB(received, 1));
|
||||||
|
|
||||||
|
EncryptionSynchronizationSequence sequence = processor.getSequence();
|
||||||
|
assertNotNull(sequence, "Trial " + trial);
|
||||||
|
assertEquals(getExpected(information), sequence.getMessage().toHexString());
|
||||||
|
assertEquals(0x80, sequence.getAlgorithmId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.message.isch;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import java.util.Random;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit tests for the I-ISCH (40,9,16) codeword decoder.
|
||||||
|
*/
|
||||||
|
public class ISCHDecoderTest
|
||||||
|
{
|
||||||
|
private static final long CODE_WORD_OFFSET = 0x184229d461L;
|
||||||
|
private final ISCHDecoder mDecoder = new ISCHDecoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the transmitted 40-bit codeword for the 9-bit message value.
|
||||||
|
*/
|
||||||
|
private static BinaryMessage encode(int value)
|
||||||
|
{
|
||||||
|
BinaryMessage message = new BinaryMessage(9);
|
||||||
|
message.load(0, 9, value);
|
||||||
|
BinaryMessage codeword = new BinaryMessage(40);
|
||||||
|
codeword.load(0, 40, ISCHDecoder.getCodeWord(message) ^ CODE_WORD_OFFSET);
|
||||||
|
return codeword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Channel number (1 or 2) for the message value, or 0 for an ISCH location (3) that isn't used for I-ISCH.
|
||||||
|
*/
|
||||||
|
private static int getChannel(int value)
|
||||||
|
{
|
||||||
|
int ischLocation = (value >> 3) & 0x3;
|
||||||
|
return ischLocation == 3 ? 0 : ((value >> 5) & 0x3) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void correctsUpToSevenBitErrors()
|
||||||
|
{
|
||||||
|
Random random = new Random(1);
|
||||||
|
|
||||||
|
for(int value = 0; value < 512; value++)
|
||||||
|
{
|
||||||
|
int channel = getChannel(value);
|
||||||
|
|
||||||
|
if(channel != 1 && channel != 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int errors = 0; errors <= 7; errors++)
|
||||||
|
{
|
||||||
|
for(int trial = 0; trial < 10; trial++)
|
||||||
|
{
|
||||||
|
BinaryMessage codeword = encode(value);
|
||||||
|
int flipped = 0;
|
||||||
|
|
||||||
|
while(flipped < errors)
|
||||||
|
{
|
||||||
|
int bit = random.nextInt(40);
|
||||||
|
|
||||||
|
if(codeword.get(bit))
|
||||||
|
{
|
||||||
|
codeword.clear(bit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
codeword.set(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
flipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrectedBinaryMessage decoded = mDecoder.decode(codeword, channel);
|
||||||
|
assertEquals(value, decoded.getInt(0, 8), "Value [" + value + "] errors [" + errors + "]");
|
||||||
|
assertTrue(decoded.getCorrectedBitCount() <= errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsWrongTimeslotCodewords()
|
||||||
|
{
|
||||||
|
for(int value = 0; value < 512; value++)
|
||||||
|
{
|
||||||
|
int channel = getChannel(value);
|
||||||
|
|
||||||
|
if(channel == 1 || channel == 2)
|
||||||
|
{
|
||||||
|
//A codeword for the other timeslot is at least 16 bits from every codeword for the expected timeslot
|
||||||
|
CorrectedBinaryMessage decoded = mDecoder.decode(encode(value), channel == 1 ? 2 : 1);
|
||||||
|
assertTrue(decoded.getCorrectedBitCount() >= 8, "Value [" + value + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.sync;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit tests for the P25 Phase 2 soft sync detector scalar and SIMD vector implementations.
|
||||||
|
*/
|
||||||
|
public class P25P2SoftSyncDetectorTest
|
||||||
|
{
|
||||||
|
private static final float IDEAL_SCORE = (float)(20 * Math.pow(3.0 * Math.PI / 4.0, 2));
|
||||||
|
|
||||||
|
private static P25P2SoftSyncDetector[] getDetectors()
|
||||||
|
{
|
||||||
|
return new P25P2SoftSyncDetector[]{new P25P2SoftSyncDetectorScalar(), new P25P2SoftSyncDetectorVector64(),
|
||||||
|
new P25P2SoftSyncDetectorVector128(), new P25P2SoftSyncDetectorVector256(),
|
||||||
|
new P25P2SoftSyncDetectorVector512()};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void detectsSyncPattern()
|
||||||
|
{
|
||||||
|
for(P25P2SoftSyncDetector detector : getDetectors())
|
||||||
|
{
|
||||||
|
float[] pattern = detector.getPatternSymbols();
|
||||||
|
float score = 0;
|
||||||
|
|
||||||
|
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
|
||||||
|
for(int repeat = 0; repeat < 3; repeat++)
|
||||||
|
{
|
||||||
|
for(float symbol : pattern)
|
||||||
|
{
|
||||||
|
score = detector.process(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vectorImplementationsMatchScalar()
|
||||||
|
{
|
||||||
|
P25P2SoftSyncDetector[] detectors = getDetectors();
|
||||||
|
Random random = new Random(1);
|
||||||
|
|
||||||
|
for(int x = 0; x < 1000; x++)
|
||||||
|
{
|
||||||
|
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
|
||||||
|
float expected = detectors[0].process(symbol);
|
||||||
|
|
||||||
|
for(int d = 1; d < detectors.length; d++)
|
||||||
|
{
|
||||||
|
assertEquals(expected, detectors[d].process(symbol), 0.001,
|
||||||
|
detectors[d].getClass().getSimpleName() + " at symbol " + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user