#2524 P25P2 Decoder enhancements (#2525)

* Adopts same architecture and symbol processor as P1 C4FM/LSM, DMR and NXDN.
* Soft symbol sync detection, processing and erro correction.
* Scalar and vector soft sync detection and calibration
* Improved time slot detection/prediction and ISCH processing.
* Fixes bug in parsing sequential mac structures in SACCH
* Vector calibration(s) cleanup across multiple calibrations
* Error correction bug in ISCH
* Renames differential demod shared with P1 LSM decoder.

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-29 07:32:17 -04:00
committed by GitHub
parent 6b9cd2d317
commit 9a6dc2b0a0
52 changed files with 4026 additions and 1314 deletions

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.dsp.c4fm.DQPSKDifferentialDemodulator;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
@@ -76,7 +77,7 @@ public class P25P1DecoderLSM extends FeedbackDecoder implements IByteBufferProvi
private static final int SYMBOL_RATE = 4800;
private final P25P1SymbolProcessorLSM mSymbolProcessor;
private final P25P1DifferentialDemodulatorLSM mDemodulator = new P25P1DifferentialDemodulatorLSM();
private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator();
private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer();
private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor();
private IRealDecimationFilter mDecimationFilterI;

View File

@@ -1,371 +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.phase1;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
/**
* P25 Phase 1 LSM (CQPSK) differential demodulator that produces a real valued sample stream for the C4FM symbol
* processor. Each output sample is the phase change across the preceding symbol period, in radians per sample (ie
* 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.
*
* Processing steps:
*
* 1. FM discriminator (atan2) produces the phase change between consecutive samples. A full range (atan2)
* discriminator is required because the LSM signal trajectory passes near the origin between symbols where the phase
* change between samples can exceed PI/2, which would fold with an atan(Q/I) discriminator.
* 2. Coarse frequency offset removal: subtracts the estimated carrier offset from each discriminator sample.
* 3. One symbol period integrator: the moving sum of the discriminator output is the phase change across the symbol
* period: arg(x[n] * conj(x[n - samplesPerSymbol])), modulo 2 * PI.
* 4. Phase wrap to +/- PI. The moving sum is only correct modulo 2 * PI since a discriminator sample near the origin
* can be wrapped by 2 * PI, and that error persists across the integrator span.
* 5. Carrier offset estimation: the fourth power of the differential QPSK symbols removes the modulation so that the
* residual phase is four times the carrier offset (radians per symbol). This is a constellation domain estimator,
* which isn't biased by the spectral tilt of a frequency selective (ie simulcast multipath) channel the way that a
* spectral centroid (ie mean discriminator output) estimator is.
*
* The fourth power estimator is ambiguous modulo PI/2 radians per symbol (1200 Hz at 4800 baud), so the estimator only
* resolves offsets within +/- 600 Hz. When a signal is present (ie the fourth power coherence exceeds the noise
* threshold) but the symbol processor is unable to synchronize, the estimate is stepped by +/- PI/2 radians per symbol
* to search the adjacent offset ranges. An offset just beyond the range aliases to an estimate near the opposite edge
* of the range (ie -650 Hz estimates as +550 Hz), so the search first steps toward the adjacent range nearest to the
* estimate (ie -1200 Hz for a positive estimate) and then the other adjacent range. The fourth power coherence is the
* same at each ambiguous offset, so high coherence without synchronization indicates an incorrect offset range, while
* low coherence indicates that there is no signal. The search holds the current offset range while there is no signal
* (ie between transmissions on a traffic channel) so that the next transmission starts with the offset range that last
* synchronized. Once an offset range is verified by a valid NID decode (see offsetVerified()), the search uses a
* longer dwell period before stepping away from it. Symbol processor synchronization alone doesn't verify the offset
* range since a false sync detection in an incorrect offset range would otherwise hold the search in that range for the
* longer dwell period.
*
* 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
* its power, so the noise variance of the phase change is proportional to 1/|x[n]|^2 + 1/|x[n - samplesPerSymbol]|^2.
* The reliability (inverse noise variance) of each demodulated sample, (|x[n]|^2 * |x[n - samplesPerSymbol]|^2) /
* (|x[n]|^2 + |x[n - samplesPerSymbol]|^2), weights the soft decision decoding (see getReliabilities()). The LSM
* signal envelope passes near the origin between symbols and simulcast multipath produces deep envelope fades, so the
* symbol error rate varies by orders of magnitude with the envelope. Testing against a simulcast control channel
* recording found that 60% of the symbol errors occur in the 15% of the symbols with the lowest envelope.
*
* Since the carrier offset is removed ahead of the symbol processor, the symbol processor's balance (PLL) value only
* represents the residual offset. Use getFrequencyOffset() to include the coarse offset when reporting the carrier
* offset to the tuner.
*/
public class P25P1DifferentialDemodulatorLSM
{
private static final double TWO_PI = 2.0 * Math.PI;
//Carrier offset ambiguity step of the fourth power estimator, in radians per symbol
private static final double AMBIGUITY_STEP = Math.PI / 2.0;
//The estimator, signal presence detection and ambiguity search update once per block of symbols, independent of
//the sample buffer size. The coherence (magnitude of the averaged unit vectors) of noise depends on the number of
//samples averaged, so a fixed block length also keeps the signal presence threshold independent of buffer size.
private static final int ESTIMATOR_BLOCK_SYMBOLS = 200;
//Coarse offset estimator loop gain applied to each block's fourth power phase error while the symbol processor is
//synchronized (~6,600 symbol time constant)
private static final double ESTIMATOR_GAIN = 0.03;
//Estimator loop gain while the symbol processor is unsynchronized, so that the estimate converges within the
//ambiguity search dwell period. With the tracking gain, the estimate converged over several dwell periods and an
//offset near the edge of the estimator range (ie -550 Hz) took longer to acquire than offsets beyond the range.
private static final double ESTIMATOR_ACQUISITION_GAIN = 0.2;
//Block coherence at and above which the estimator applies the full loop gain
private static final double ESTIMATOR_FULL_GAIN_COHERENCE = 0.3;
//Minimum average fourth power coherence (magnitude of the averaged unit vectors) to detect signal presence, to update
//the estimate and to advance the ambiguity search. LSM recordings produce values above 0.3 without injected noise
//and average 0.11-0.14 at 0 dB carrier to noise (50 kHz). Single block coherence values for noise reach 0.10, so
//the coherence is averaged across blocks, where the maximum noise value is 0.041 across 10 minutes of noise.
private static final double SIGNAL_MINIMUM_COHERENCE = 0.07;
//Coherence averaging gain, applied per block (~10 blocks or ~2,000 symbols)
private static final double COHERENCE_AVERAGING_GAIN = 0.1;
//Symbol count with a signal present but without synchronization before stepping to the next ambiguity search offset
private static final int SEARCH_DWELL_SYMBOLS = 2000;
//Symbol count with a signal present but without synchronization before stepping away from an offset range that
//was verified (~5 seconds). At low SNR, sync is frequently lost and reacquired, and the shorter search dwell caused
//the search to spend half of the time in an incorrect offset range at 0 dB carrier to noise.
private static final int VERIFIED_DWELL_SYMBOLS = 24000;
//Ambiguity search offsets, as multiples of the ambiguity step, relative to the estimator's offset. The search
//direction sets the sign of the adjacent range steps.
private static final int[] SEARCH_STEPS = {0, 1, -1};
private IRealFilter mIntegrator;
//Sample power history spanning the reliability symbol period delay, carried across sample buffers
private float[] mPowerHistory = new float[1];
private float[] mReliabilities = new float[0];
private double mSamplesPerSymbol = 1.0;
private float mPreviousI;
private float mPreviousQ;
//Fourth power estimator offset, radians per symbol
private double mEstimatorOffset = 0.0;
private int mSearchIndex = 0;
private int mSearchDirection = 1;
private double mUnsynchronizedSignalSymbols = 0;
//Indicates the current search offset range was verified by a valid NID decode since it was selected
private boolean mSearchIndexVerified = false;
private double mAverageCoherence = 0.0;
//Block length in samples and the fourth power accumulation for the block under accumulation
private int mBlockLength = ESTIMATOR_BLOCK_SYMBOLS;
private int mBlockPointer = 0;
private double mBlockI = 0.0;
private double mBlockQ = 0.0;
/**
* Constructs an instance
*/
public P25P1DifferentialDemodulatorLSM()
{
}
/**
* Sets or updates the samples per symbol.
* @param samplesPerSymbol for the incoming sample stream
*/
public void setSamplesPerSymbol(double samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mIntegrator = new RealFIRFilter(FilterFactory.getDifferentialIntegrator(samplesPerSymbol));
mPowerHistory = new float[Math.max(1, (int)Math.round(samplesPerSymbol))];
mBlockLength = (int)Math.round(ESTIMATOR_BLOCK_SYMBOLS * samplesPerSymbol);
resetBlock();
}
/**
* Resets the carrier offset estimate after a tuner center frequency change.
*/
public void reset()
{
mEstimatorOffset = 0.0;
mSearchIndex = 0;
mSearchDirection = 1;
mUnsynchronizedSignalSymbols = 0;
mSearchIndexVerified = false;
mAverageCoherence = 0.0;
resetBlock();
}
/**
* Resets the fourth power accumulation for the block under accumulation.
*/
private void resetBlock()
{
mBlockPointer = 0;
mBlockI = 0.0;
mBlockQ = 0.0;
}
/**
* Notification that a valid NID was decoded, verifying the current carrier offset range so that the ambiguity search
* uses the longer dwell period before stepping away from it.
*/
public void offsetVerified()
{
mSearchIndexVerified = true;
}
/**
* Carrier offset (coarse) removed by this demodulator.
* @return offset in radians per sample.
*/
public float getFrequencyOffset()
{
return (float)(getOffset() / mSamplesPerSymbol);
}
/**
* Soft symbol reliabilities for each of the samples produced by the most recent demodulate() call.
* @return reliabilities (inverse of the relative phase noise variance), parallel to the demodulated samples.
*/
public float[] getReliabilities()
{
return mReliabilities;
}
/**
* Current total offset including the ambiguity search step.
* @return offset in radians per symbol.
*/
private double getOffset()
{
return mEstimatorOffset + SEARCH_STEPS[mSearchIndex] * mSearchDirection * AMBIGUITY_STEP;
}
/**
* Demodulates the filtered complex samples.
* @param i inphase samples
* @param q quadrature samples
* @param processorSynchronized indicates if the symbol processor is currently synchronized to the signal, which
* controls the ambiguity search.
* @return differentially demodulated samples in radians per sample.
*/
public float[] demodulate(float[] i, float[] q, boolean processorSynchronized)
{
float[] discriminated = new float[i.length];
float previousI = mPreviousI;
float previousQ = mPreviousQ;
float demodulatedI, demodulatedQ;
for(int x = 0; x < i.length; x++)
{
//Multiply the current sample by the complex conjugate of the previous sample
demodulatedI = (i[x] * previousI) + (q[x] * previousQ);
demodulatedQ = (q[x] * previousI) - (i[x] * previousQ);
discriminated[x] = (float)Math.atan2(demodulatedQ, demodulatedI);
previousI = i[x];
previousQ = q[x];
}
mPreviousI = previousI;
mPreviousQ = previousQ;
mReliabilities = calculateReliabilities(i, q);
float[] demodulated = mIntegrator.filter(discriminated);
//Remove the carrier offset (the integrator has unity gain at DC, so the offset is removed after integration to
//apply each block's updated estimate), wrap each sample to +/- PI radians per symbol, and accumulate the fourth
//power of each symbol phase.
double samplesPerSymbol = mSamplesPerSymbol;
float wrap = (float)(TWO_PI / samplesPerSymbol);
float offset = getFrequencyOffset();
double theta;
for(int x = 0; x < demodulated.length; x++)
{
demodulated[x] -= offset;
demodulated[x] -= wrap * Math.round(demodulated[x] / wrap);
theta = 4.0 * demodulated[x] * samplesPerSymbol;
mBlockI += Math.cos(theta);
mBlockQ += Math.sin(theta);
if(++mBlockPointer >= mBlockLength)
{
updateEstimate(processorSynchronized);
offset = getFrequencyOffset();
}
}
return demodulated;
}
/**
* Updates the carrier offset estimate, the signal presence detection and the ambiguity search from the fourth power
* accumulation for the completed block.
* @param processorSynchronized state of the symbol processor
*/
private void updateEstimate(boolean processorSynchronized)
{
//Ideal symbols (+/- PI/4 and +/- 3PI/4) all map to PI in the fourth power, so the residual angle is four times
//the residual carrier offset (radians per symbol).
double coherence = Math.sqrt(mBlockI * mBlockI + mBlockQ * mBlockQ) / mBlockPointer;
mAverageCoherence += (coherence - mAverageCoherence) * COHERENCE_AVERAGING_GAIN;
boolean signalPresent = mAverageCoherence > SIGNAL_MINIMUM_COHERENCE;
if(signalPresent)
{
//Weight the update by this block's coherence so that noise blocks (ie after a transmission ends, while the
//average coherence decays) don't move the estimate.
double error = Math.atan2(-mBlockQ, -mBlockI) / 4.0;
double weight = Math.min(1.0, coherence / ESTIMATOR_FULL_GAIN_COHERENCE);
double gain = processorSynchronized ? ESTIMATOR_GAIN : ESTIMATOR_ACQUISITION_GAIN;
mEstimatorOffset += error * gain * weight;
//Constrain the estimator to the fundamental range. The ambiguity search covers the adjacent ranges.
mEstimatorOffset = Math.clamp(mEstimatorOffset, -AMBIGUITY_STEP / 2, AMBIGUITY_STEP / 2);
}
updateSearch(mBlockPointer, processorSynchronized, signalPresent);
resetBlock();
}
/**
* Calculates the reliability of each demodulated sample from the power of the samples at either end of the symbol
* period: (|x[n]|^2 * |x[n - samplesPerSymbol]|^2) / (|x[n]|^2 + |x[n - samplesPerSymbol]|^2)
* @param i inphase samples
* @param q quadrature samples
* @return reliabilities
*/
private float[] calculateReliabilities(float[] i, float[] q)
{
float[] history = mPowerHistory;
int delay = history.length;
float[] power = new float[i.length];
float[] reliabilities = new float[i.length];
float current, delayed, sum;
for(int x = 0; x < i.length; x++)
{
power[x] = i[x] * i[x] + q[x] * q[x];
}
for(int x = 0; x < i.length; x++)
{
current = power[x];
delayed = x >= delay ? power[x - delay] : history[x];
sum = current + delayed;
reliabilities[x] = sum > 0.0f ? current * delayed / sum : 0.0f;
}
//Retain the final (delay) sample powers for the next buffer, shifting forward when the buffer is short
if(i.length >= delay)
{
System.arraycopy(power, i.length - delay, history, 0, delay);
}
else
{
System.arraycopy(history, i.length, history, 0, delay - i.length);
System.arraycopy(power, 0, history, delay - i.length, i.length);
}
return reliabilities;
}
/**
* Steps the ambiguity search offset when a signal is present and the symbol processor hasn't synchronized within
* the dwell period. Holds the current search offset while there is no signal.
* @param sampleCount in the completed block
* @param processorSynchronized state of the symbol processor
* @param signalPresent indicates if a signal is present
*/
private void updateSearch(int sampleCount, boolean processorSynchronized, boolean signalPresent)
{
if(processorSynchronized)
{
mUnsynchronizedSignalSymbols = 0;
return;
}
if(signalPresent)
{
mUnsynchronizedSignalSymbols += sampleCount / mSamplesPerSymbol;
if(mUnsynchronizedSignalSymbols > (mSearchIndexVerified ? VERIFIED_DWELL_SYMBOLS : SEARCH_DWELL_SYMBOLS))
{
mUnsynchronizedSignalSymbols = 0;
mSearchIndexVerified = false;
mSearchIndex = (mSearchIndex + 1) % SEARCH_STEPS.length;
//Leaving the estimator's range: step first toward the adjacent range nearest to the estimate
if(mSearchIndex == 1)
{
mSearchDirection = mEstimatorOffset > 0.0 ? -1 : 1;
}
}
}
}
}

View File

@@ -24,7 +24,7 @@ import io.github.dsheirer.module.decode.FeedbackDecoder;
/**
* P25 Phase 1 LSM implementation of the symbol processor. Processes the differentially demodulated sample stream
* produced by the P25P1DifferentialDemodulatorLSM, which is in the same radians per sample domain as the C4FM decoder's
* produced by the DQPSKDifferentialDemodulator, which is in the same radians per sample domain as the C4FM decoder's
* sample stream, so this processor shares the P25 C4FM sync detection, optimization and tuning parameters.
*
* Note: testing against LSM recordings without injected noise showed no meaningful difference across equalizer spans

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.module.decode.p25.phase1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
@@ -29,27 +28,33 @@ import jdk.incubator.vector.VectorSpecies;
*/
public class P25P1SoftSyncDetectorVector512 extends P25P1SoftSyncDetector
{
private static final boolean[] VECTOR_MASK = new boolean[]{false, false, false, false, false, false, false, false,
true, true, true, true, true, true, true, true};
private static final VectorSpecies<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
* symbols that include this recent soft symbol.
* Constructs an instance
*/
public P25P1SoftSyncDetectorVector512()
{
System.arraycopy(getPatternSymbols(), 16, mTailPatternSymbols, 8, 8);
}
/**
* Calculates the correlation of the preceding 24 soft symbols against the sync pattern.
*
* @return correlation score.
*/
@Override
public float calculate()
{
//There are 24 symbols that we load into 2x vectors of 16. We load the first 16 symbols into vector 1 and the
//remaining 8 symbols into vector 2 using a vector mask that ignores the first 8 symbols and loads the second 8
// symbols. This sets the first 8 symbols to zero and those values are ignored during calculations.
VectorMask<Float> mask = VectorMask.fromArray(VECTOR_SPECIES, VECTOR_MASK, 0);
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer)
.fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0), accumulator);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8, mask)
.fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 8), accumulator);
//There are 24 symbols that we load into 2x vectors of 16. The second vector overlaps the first vector by 8
//symbols (8-15), so it is multiplied against the final 8 pattern symbols (16-23) zero padded in the first 8
//lanes. Multiplying against zero is cheaper than a masked load.
FloatVector accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer)
.mul(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0));
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mTailPatternSymbols, 0), accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,11 +26,16 @@ import io.github.dsheirer.dsp.c4fm.DibitSyncDetector;
*/
public abstract class P25P1SyncDetector extends DibitSyncDetector
{
/**
* P25 Phase 1 sync pattern: +33333 -3 +33 -33 +33 -3333 +3 -3 +3 -33333
*/
public static final long SYNC_PATTERN = 0x5575F5FF77FFL;
/**
* Constructs an instance
*/
public P25P1SyncDetector()
{
super(0x5575F5FF77FFL, 24);
super(SYNC_PATTERN, 24);
}
}

View File

@@ -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;
}
}

View File

@@ -18,24 +18,24 @@
*/
package io.github.dsheirer.module.decode.p25.phase2;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.channel.state.MultiChannelState;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.dsp.c4fm.DQPSKDifferentialDemodulator;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.gain.complex.ComplexGainFactory;
import io.github.dsheirer.dsp.gain.complex.IComplexGainControl;
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.patch.PatchGroupManager;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
@@ -43,10 +43,14 @@ import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
@@ -54,85 +58,119 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Phase 2 HDQPSK 2-timeslot Decoder
* APCO25 Phase 2 HDQPSK 2-timeslot decoder. Decimates incoming sample buffers to 4-8 samples per symbol. Employs a
* flat baseband (channel) filter without a matched (root raised cosine) filter since the differential demodulator is a
* limiter-discriminator receiver where narrowing the bandwidth ahead of the discriminator distorts the phase trajectory.
* The differential demodulator (mDemodulator) removes the coarse carrier offset and differentially demodulates the
* filtered complex baseband (I/Q) sample stream into the phase change across each symbol period, in the same radians
* per sample domain as the C4FM decoders. This allows the decoder to share the C4FM symbol processor framework
* (mSymbolProcessor) that performs sync detection, timing correction, equalization and balance (PLL) residual mistune
* correction. The message framer frames super frame fragments and a registered message listener receives the decoded
* messages.
*
* As a child of the FeedbackDecoder, this decoder provides periodic PLL measurements to the tuner for automatic PPM
* correction. It also provides a stream of demodulated soft symbols (in radians) for display to the user.
*/
public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdateListener
public class P25P2DecoderHDQPSK extends FeedbackDecoder implements ISourceEventListener, IComplexSamplesListener,
Listener<ComplexSamples>, IByteBufferProvider, IdentifierUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(P25P2DecoderHDQPSK.class);
protected static final float SYMBOL_TIMING_GAIN = 0.1f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKGardnerDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected P25P2MessageFramer mMessageFramer;
protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl();
protected IRealFilter mIBasebandFilter;
protected IRealFilter mQBasebandFilter;
private DecodeConfigP25Phase2 mDecodeConfigP25Phase2;
private FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
//Baseband filter settings. The filter precedes the differential demodulator's carrier offset removal, so the pass
//band must accommodate the carrier offset. Testing against Phase 2 recordings (pass/stop: total valid MAC messages)
//5500/7000: 9210, 6000/7500: 9273, 6500/7800: 9294, 7000/8500: 9262. Narrower pass bands lost the most valid
//messages on a recording with a 1.8 kHz carrier offset.
private static final double BASEBAND_FILTER_PASS_BAND = 6500.0; //Hertz
private static final double BASEBAND_FILTER_STOP_BAND = 7800.0; //Hertz
private static final int SYMBOL_RATE = 6000;
private final P25P2SymbolProcessor mSymbolProcessor;
private final DQPSKDifferentialDemodulator mDemodulator = new DQPSKDifferentialDemodulator();
private final P25P2MessageFramer mMessageFramer = new P25P2MessageFramer();
private final P25P2MessageProcessor mMessageProcessor = new P25P2MessageProcessor();
private final DecodeConfigP25Phase2 mDecodeConfigP25Phase2;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
/**
* Constructs an instance
* @param decodeConfigP25Phase2 with optional scramble parameters
*/
public P25P2DecoderHDQPSK(DecodeConfigP25Phase2 decodeConfigP25Phase2)
{
super(6000.0);
setSampleRate(25000.0);
mDecodeConfigP25Phase2 = decodeConfigP25Phase2;
}
mMessageProcessor.setMessageListener(getMessageListener());
mMessageFramer.setListener(mMessageProcessor);
mSymbolProcessor = new P25P2SymbolProcessor(this, mMessageFramer);
@Override
public void start()
{
super.start();
mQPSKDemodulator.start();
//Refresh the scramble parameters each time we start in case they change
if(mDecodeConfigP25Phase2 != null && mDecodeConfigP25Phase2.getScrambleParameters() != null &&
!mDecodeConfigP25Phase2.isAutoDetectScrambleParameters())
if(mDecodeConfigP25Phase2 != null)
{
mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters());
}
setSampleRate(25000.0);
}
@Override
public void stop()
public String getProtocolDescription()
{
super.stop();
mQPSKDemodulator.stop();
return "P25 Phase 2";
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE2;
}
/**
* Preloads P25 frequency bands when this is a traffic channel, since they are not transmitted on the traffic
* channel and the decoder state needs accurate frequency values to pass to the traffic channel manager for
* call event tracking.
* @param preLoadDataContent with known frequency bands.
*/
@Subscribe
public void process(P25FrequencyBandPreloadDataContent preLoadDataContent)
{
mMessageProcessor.preload(preLoadDataContent);
}
/**
* Sets the sample rate and configures internal components.
* @param sampleRate of the channel to decode
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
//Note: the baseband filter is designed for a 50 kHz sample rate, independent of the current sample rate.
float[] basebandTaps = FilterFactory.getRemezLowPass(50000.0, 6500, 0.005, 7200, 0.01);
mIBasebandFilter = FilterFactory.getRealFilter(basebandTaps);
mQBasebandFilter = FilterFactory.getRealFilter(basebandTaps);
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
if(mMessageFramer != null)
if(sampleRate <= SYMBOL_RATE * 2)
{
getDibitBroadcaster().removeListener(mMessageFramer);
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >12000 (2 * " +
SYMBOL_RATE + " symbol rate)");
}
//The Costas Loop receives symbol-inversion correction requests when detected.
//The PLL gain monitor receives sync detect/loss signals from the message framer
mMessageFramer = new P25P2MessageFramer(mCostasLoop);
int decimation = 1;
if(mDecodeConfigP25Phase2 !=null)
//Identify decimation that gets as close to 4.0 Samples Per Symbol as possible (6000 x 4.0 = 24 kHz)
while((sampleRate / decimation) >= 48000)
{
mMessageFramer.setScrambleParameters(mDecodeConfigP25Phase2.getScrambleParameters());
decimation *= 2;
}
mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this);
mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
mMessageFramer.setListener(getMessageProcessor());
mMessageFramer.setSampleRate(sampleRate);
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
float decimatedSampleRate = (float)sampleRate / decimation;
//Set the decimated sample rate to use for PLL error reporting.
setDecimatedSampleRate(decimatedSampleRate);
float[] basebandTaps = FilterFactory.getRemezLowPass(decimatedSampleRate, BASEBAND_FILTER_PASS_BAND,
BASEBAND_FILTER_STOP_BAND);
mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps);
mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps);
float samplesPerSymbol = decimatedSampleRate / (float)SYMBOL_RATE;
mDemodulator.setSamplesPerSymbol(samplesPerSymbol);
mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol);
}
/**
@@ -142,25 +180,118 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
@Override
public void receive(ComplexSamples samples)
{
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
float[] i = mDecimationFilterI.decimateReal(samples.i());
float[] q = mDecimationFilterQ.decimateReal(samples.q());
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
//Differential demodulation: phase change across each symbol period (radians per sample)
float[] demodulated = mDemodulator.demodulate(i, q, mSymbolProcessor.isSynchronized());
//Process demodulated samples into symbols and apply sync detection and message framing.
mSymbolProcessor.process(demodulated);
}
/**
* Includes the coarse carrier offset that the differential demodulator removes ahead of the symbol processor,
* since the symbol processor's PLL error only represents the residual offset. The symbol processor invokes this
* method when the message framer produces a valid super frame fragment, which also verifies the demodulator's
* carrier offset range.
* @param pllError residual carrier offset (radians per sample) from the symbol processor
*/
@Override
public void processPLLError(float pllError)
{
mDemodulator.offsetVerified();
super.processPLLError(pllError - mDemodulator.getFrequencyOffset());
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void setBufferListener(Listener<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
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())
{
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mCostasLoop.reset();
setSampleRate(sourceEvent.getValue().doubleValue());
case NOTIFICATION_FREQUENCY_CHANGE:
mSymbolProcessor.resetBalance();
mDemodulator.reset();
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
mCostasLoop.reset();
//Tuner PPM changes are small and the carrier offset estimator and symbol processor balance track the
//change, so a reset would only discard the carrier offset estimate.
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
}
}
@@ -171,22 +302,18 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
@Override
public void reset()
{
mCostasLoop.reset();
mSymbolProcessor.resetBalance();
mDemodulator.reset();
}
@Override
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
{
return new Listener<IdentifierUpdateNotification>()
{
@Override
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
return identifierUpdateNotification -> {
if(identifierUpdateNotification.getIdentifier().getForm() == Form.SCRAMBLE_PARAMETERS)
{
if(identifierUpdateNotification.getIdentifier().getForm() == Form.SCRAMBLE_PARAMETERS && mMessageFramer != null)
{
ScrambleParameters scrambleParameters = (ScrambleParameters)identifierUpdateNotification.getIdentifier().getValue();
mMessageFramer.setScrambleParameters(scrambleParameters);
}
ScrambleParameters scrambleParameters = (ScrambleParameters)identifierUpdateNotification.getIdentifier().getValue();
mMessageFramer.setScrambleParameters(scrambleParameters);
}
};
}
@@ -200,17 +327,17 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2();
channel.setDecodeConfiguration(config);
Path path = Paths.get("/home/denny/temp/20240309_054833_855787500_Duke_Energy_Monroe_Dicks_Creek_Site_201-24_1_baseband.wav");
// ScrambleParameters scrambleParameters = new ScrambleParameters(0x91F14, 0x201, 0x00A);
// config.setScrambleParameters(scrambleParameters);
Path directory = Paths.get("/run/media/denny/T9/P25P2 Baseband Samples/");
Path path = directory.resolve("20240303_203244_859412500_Duke_Energy_P25_Lake_Control_28_baseband.wav");
// ScrambleParameters pcwinScrambleParameters = new ScrambleParameters(0xBEE00, 0x3BB, 0x3B2);
// Path path = directory.resolve("20261029_043353_853425000_PCWIN_Tucson_TCH_168_baseband.wav");
// Path path = directory.resolve("20261029_044105_851562500_PCWIN_Tucson_TCH_168_baseband.wav");
// Path path = directory.resolve("20261029_044126_851612500_PCWIN_Tucson_TCH_289_baseband.wav");
// Path path = directory.resolve("20261029_044556_852450000_PCWIN_Tucson_TCH_168_baseband.wav");
// Path path = directory.resolve("20261029_044902_853175000_PCWIN_Tucson_TCH_168_baseband.wav");
// Path path = Paths.get("/home/denny/temp/PA-STARNet_ECEN_TRAFFIC_10_baseband_20220610_104744.wav");
// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 2370, 2369);
// config.setScrambleParameters(scrambleParameters);
// Path path = Paths.get("/home/denny/temp/20240303_203244_859412500_Duke_Energy_P25_Lake_Control_28_baseband.wav");
// ScrambleParameters scrambleParameters = new ScrambleParameters(0x91F14, 0x2D7, 0x00A);
// config.setScrambleParameters(scrambleParameters);
AliasList aliasList = new AliasList("bogus");
ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,9 +21,8 @@ package io.github.dsheirer.module.decode.p25.phase2;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.identifier.patch.PatchGroupManager;
import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.message.IMessage;
@@ -49,52 +48,29 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors
* and issue phase corrections.
* P25 Phase 2 message framer. Receives symbol decisions (dibits) and sync detection notifications from the symbol
* processor and frames super frame fragments using the super frame detector.
*/
public class P25P2MessageFramer implements Listener<Dibit>
public class P25P2MessageFramer extends C4FMMessageFramer
{
private final static Logger mLog = LoggerFactory.getLogger(P25P2MessageFramer.class);
private P25P2SuperFrameDetector mSuperFrameDetector;
private final P25P2SuperFrameDetector mSuperFrameDetector = new P25P2SuperFrameDetector();
private boolean mRunning = false;
/**
* Constructs an instance
* @param phaseLockedLoop for use with the super frame detector
*/
public P25P2MessageFramer(IPhaseLockedLoop phaseLockedLoop)
public P25P2MessageFramer()
{
mSuperFrameDetector = new P25P2SuperFrameDetector(phaseLockedLoop);
}
/**
* Sets or updates the scramble parameters for the current channel
* @param scrambleParameters
* @param scrambleParameters to apply
*/
public void setScrambleParameters(ScrambleParameters scrambleParameters)
{
if(mSuperFrameDetector != null)
{
mSuperFrameDetector.setScrambleParameters(scrambleParameters);
}
}
/**
* Sets the sample rate for the sync detector
*/
public void setSampleRate(double sampleRate)
{
mSuperFrameDetector.setSampleRate(sampleRate);
}
/**
* Registers a sync detect listener to be notified each time sync is detected or lost.
*
* Note: this actually registers the listener on the enclosed super frame detector which has access to the
* actual sync pattern detector instance.
*/
public void setSyncDetectListener(ISyncDetectListener listener)
{
mSuperFrameDetector.setSyncDetectListener(listener);
mSuperFrameDetector.setScrambleParameters(scrambleParameters);
}
/**
@@ -104,22 +80,87 @@ public class P25P2MessageFramer implements Listener<Dibit>
*/
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.
*
* @param dibit to process
* @return true if a valid super frame fragment was produced.
*/
@Override
public void receive(Dibit dibit)
public boolean process(Dibit dibit)
{
mSuperFrameDetector.receive(dibit);
return mSuperFrameDetector.process(dibit);
}
/**
* Primary method for streaming decoded symbol byte arrays.
* Primary method for streaming decoded symbol dibits and soft symbols for message framing.
*
* @param dibit to process
* @param softSymbol for the dibit (radians), for soft decision decoding
* @return true if a valid super frame fragment was produced.
*/
@Override
public boolean process(Dibit dibit, float softSymbol)
{
return mSuperFrameDetector.process(dibit, softSymbol);
}
/**
* Processes the dibit and detects sync patterns from the dibit stream, for use when processing a stream of dibits
* without a symbol processor (ie replaying a recorded bit stream).
*
* @param dibit to process
*/
public void processWithHardSyncDetect(Dibit dibit)
{
mSuperFrameDetector.processWithHardSyncDetect(dibit);
}
/**
* Processes the decoded symbol byte array using hard sync detection.
*
* @param buffer to process into a stream of dibits for processing.
*/
@@ -129,7 +170,7 @@ public class P25P2MessageFramer implements Listener<Dibit>
{
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();
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null);
P25P2MessageFramer messageFramer = new P25P2MessageFramer();
messageFramer.start();
messageFramer.setScrambleParameters(scrambleParameters);
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
messageFramer.setListener(messageProcessor);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,11 +19,10 @@
package io.github.dsheirer.module.decode.p25.phase2;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ISCHSequence;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.module.decode.p25.phase2.message.SuperFrameFragment;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
@@ -40,14 +39,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* APCO25 Phase 2 super-frame fragment detector uses a pair of sync pattern detectors and a circular dibit buffer to
* detect sync patterns and correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and
* 4x ISCH fragments. The first ISCH fragment consisting of 20-bits from the preceding fragment and 20-bits from
* APCO25 Phase 2 super-frame fragment detector uses sync detection notifications and a circular dibit buffer to
* correctly frame a (720-dibit / 1440-bit) super-frame fragment containing 4x timeslots and 4x ISCH fragments. Sync
* detections are provided by the symbol processor's soft sync detector (see syncDetected()) or by checking the dibit
* buffer for the sync pattern (see processWithHardSyncDetect()) when processing a stream of dibits without soft
* symbols. The first ISCH fragment consisting of 20-bits from the preceding fragment and 20-bits from
* the current fragment are reassembled into a contiguous ISCH chunk. The layout for the super frame fragment is:
*
* I-ISCH1 + TIMESLOT1 + I-ISCH2 + TIMESLOT2 + S-SISCH1(sync1) + TIMESLOT3 + S-SISCH2(sync2) + TIMESLOT4
*/
public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectListener
public class P25P2SuperFrameDetector
{
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;
/**
* Threshold for broadcasting a sync loss message once per second. Size is the quantity of dibits per second (3000)
* plus the length of one super frame (720).
* Threshold for broadcasting a sync loss message every 3000 dibits (0.5 seconds at 6000 dibits per second) when not
* synchronized. Size is the sync loss broadcast interval (3000) plus the length of one super frame fragment (720).
*/
private static final int BROADCAST_SYNC_LOSS_DIBIT_COUNT = 3720;
@@ -98,14 +99,34 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
*/
private static final int UN_SYNCHRONIZED_SYNC_MATCH_THRESHOLD = 4;
/**
* Dibit count between the final dibit of a detected sync pattern and the fragment check, which aligns the detected
* sync pattern with sync pattern 2 in the fragment buffer. This is the 160 dibits between the end of the second
* sync pattern and the end of the fragment plus the extra dibits for the oversized fragment buffer.
*/
private static final int SYNC_DETECTION_DELAY = 160 + FRAGMENT_BUFFER_OVERSIZE;
/**
* Dibits per second (6000 baud) for calculating message timestamps.
*/
private static final double DIBITS_PER_SECOND = 6000.0;
/**
* Maximum number of fragments (1.44 seconds) following a fragment with a valid I-ISCH that the ISCH sequence can be
* predicted for. Testing against Phase 2 recordings with injected noise: the predicted sequence matched the
* decoded sequence for all 5,256 fragments with a valid I-ISCH, and increasing the maximum to 48 fragments only
* added predictions at 6 dB in-band C/N.
*/
private static final int MAX_PREDICTION_FRAGMENT_COUNT = 12;
/**
* Maximum offset (dibits) from a whole number of fragments since the anchor fragment for an ISCH sequence
* prediction, which matches the dibit stuffing/deletion offset range that the fragment checks can correct.
*/
private static final int MAX_PREDICTION_DIBIT_OFFSET = 2;
private ScramblingSequence mScramblingSequence = new ScramblingSequence();
private Listener<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
@@ -113,13 +134,28 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
* stuffed or deleted dibits in the sequence.
*/
private DibitDelayBuffer mFragmentBuffer = new DibitDelayBuffer(720 + (2 * FRAGMENT_BUFFER_OVERSIZE));
private int mDibitsProcessed = 0;
private boolean mSynchronized = false;
private ISyncDetectListener mSyncDetectListener;
public P25P2SuperFrameDetector(IPhaseLockedLoop phaseLockedLoop)
/**
* Soft symbol buffer, parallel to the fragment buffer, for soft decision decoding.
*/
private final float[] mSoftSymbolBuffer = new float[720 + (2 * FRAGMENT_BUFFER_OVERSIZE)];
private int mSoftSymbolPointer = 0;
private boolean mSoftSymbolsAvailable = false;
private int mDibitsProcessed = 0;
private long mDibitCounter = 0;
private long mAnchorDibitCounter = -1;
private ISCHSequence mAnchorIschSequence = ISCHSequence.UNKNOWN;
private int mSyncDetectionCountdown = 0;
private int mDibitsSinceTimestamp = 0;
private long mReferenceTimestamp = 0;
private boolean mSynchronized = false;
private boolean mValidFragment = false;
/**
* Constructs an instance
*/
public P25P2SuperFrameDetector()
{
mSyncDetector = new P25P2SyncDetector(this, phaseLockedLoop);
}
/**
@@ -131,89 +167,154 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
mScramblingSequence.update(scramblingSequence);
}
/**
* Sets an external sync detect listener to receive sync detect/loss notifications
*/
public void setSyncDetectListener(ISyncDetectListener listener)
{
mSyncDetectListener = listener;
}
public void setListener(Listener<IMessage> listener)
{
mMessageListener = listener;
}
/**
* Sets the sample rate for the phase inversion sync detector
* Sets or updates the current dibit stream time from an incoming sample buffer.
* @param timestamp to use as a reference timestamp.
*/
public void setSampleRate(double sampleRate)
public void setTimestamp(long timestamp)
{
mSyncDetector.setSampleRate(sampleRate);
}
public void reset()
{
}
@Override
public void syncDetected(int bitErrors)
{
checkFragmentSync(bitErrors);
//Rebroadcast to an external listener
if(mSyncDetectListener != null)
{
mSyncDetectListener.syncDetected(bitErrors);
}
}
@Override
public void syncLost(int bitsProcessed)
{
//Rebroadcast to an external listener
if(mSyncDetectListener != null)
{
mSyncDetectListener.syncLost(bitsProcessed);
}
mReferenceTimestamp = timestamp;
mDibitsSinceTimestamp = 0;
}
/**
* Calculates the timestamp accurate to the currently received dibit.
* @return timestamp in milliseconds.
*/
private long getCurrentTimestamp()
{
//TODO: implement a dibit counter and timestamp calculator. We should receive a timestamp update with each
//TODO: buffer that arrives. Use the timestamp dibit counter to calculate the exact timestamp of where we're at.
if(mReferenceTimestamp > 0)
{
return mReferenceTimestamp + (long)(1000.0 * mDibitsSinceTimestamp / DIBITS_PER_SECOND);
}
return System.currentTimeMillis();
}
@Override
public void receive(Dibit dibit)
/**
* Notification that a sync pattern was detected where the final sync dibit is the most recently processed dibit.
* When not synchronized, schedules a fragment check once the detected sync pattern aligns with the sync pattern 2
* location in the fragment buffer. Ignored when synchronized since the fragment is checked at each fragment
* interval.
*/
public void syncDetected()
{
if(!mSynchronized && mSyncDetectionCountdown == 0)
{
mSyncDetectionCountdown = SYNC_DETECTION_DELAY;
}
}
/**
* Processes the dibit and detects sync patterns from the dibits in the fragment buffer, for use when processing a
* stream of dibits without access to the soft symbol sync detection (see syncDetected()).
* @param dibit to process
* @return true if a valid super frame fragment was produced.
*/
public boolean processWithHardSyncDetect(Dibit dibit)
{
boolean valid = process(dibit);
if(!mSynchronized && mSyncDetectionCountdown == 0)
{
int bitErrorCount = getSyncBitErrorCount(DIBIT_DELAY_BUFFER_INDEX_SYNC_2);
if(bitErrorCount <= UN_SYNCHRONIZED_SYNC_MATCH_THRESHOLD)
{
mValidFragment = false;
checkFragmentSync(bitErrorCount);
valid |= mValidFragment;
}
}
return valid;
}
/**
* Processes the dibit and the soft symbol it was decided from.
* @param dibit to process
* @param softSymbol for the dibit (radians)
* @return true if a valid super frame fragment (ie contains at least one valid MAC message) was produced.
*/
public boolean process(Dibit dibit, float softSymbol)
{
mSoftSymbolBuffer[mSoftSymbolPointer++] = softSymbol;
mSoftSymbolPointer %= mSoftSymbolBuffer.length;
mSoftSymbolsAvailable = true;
return process(dibit);
}
/**
* Extracts soft symbols from the soft symbol buffer, using the same indexing as the fragment buffer.
* @param start index relative to the oldest soft symbol in the buffer
* @param length number of soft symbols
* @return soft symbols or null if soft symbols are not available.
*/
private float[] getSoftSymbols(int start, int length)
{
if(!mSoftSymbolsAvailable)
{
return null;
}
float[] softSymbols = new float[length];
int pointer = (mSoftSymbolPointer + start) % mSoftSymbolBuffer.length;
for(int x = 0; x < length; x++)
{
softSymbols[x] = mSoftSymbolBuffer[pointer++];
pointer %= mSoftSymbolBuffer.length;
}
return softSymbols;
}
/**
* Processes the dibit.
* @param dibit to process
* @return true if a valid super frame fragment (ie contains at least one valid MAC message) was produced.
*/
public boolean process(Dibit dibit)
{
mValidFragment = false;
mDibitsProcessed++;
mDibitsSinceTimestamp++;
mDibitCounter++;
mFragmentBuffer.put(dibit);
if(mSynchronized)
{
mSyncDetectionDelayBuffer.put(dibit);
mSyncDetectionCountdown = 0;
if(mDibitsProcessed >= FRAGMENT_DIBIT_LENGTH)
{
checkFragmentSync(0);
}
}
else
else if(mSyncDetectionCountdown > 0)
{
//Only feed the sync pattern detector if we're not synchronized
Dibit delayed = mSyncDetectionDelayBuffer.getAndPut(dibit);
mSyncDetector.receive(delayed);
mSyncDetectionCountdown--;
if(mSyncDetectionCountdown == 0)
{
checkFragmentSync(getSyncBitErrorCount(DIBIT_DELAY_BUFFER_INDEX_SYNC_2));
}
}
//Broadcast sync loss message once a second (3000 dibits/6000 bits) when we're not synchronized
//Broadcast sync loss message every 3000 dibits (6000 bits) when we're not synchronized
if(mDibitsProcessed > BROADCAST_SYNC_LOSS_DIBIT_COUNT)
{
mDibitsProcessed -= 3000;
broadcastSyncLoss(3000);
}
return mValidFragment;
}
/**
@@ -234,8 +335,12 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
mDibitsProcessed = 0 + dibitOffset;
CorrectedBinaryMessage message = mFragmentBuffer.getMessage(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720);
message.setCorrectedBitCount(bitErrors);
SuperFrameFragment frameFragment = new SuperFrameFragment(message, getCurrentTimestamp(), mScramblingSequence);
float[] softSymbols = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + dibitOffset, 720);
SuperFrameFragment frameFragment = new SuperFrameFragment(message, softSymbols, getCurrentTimestamp(),
mScramblingSequence);
updateIschSequence(frameFragment, mDibitCounter - FRAGMENT_BUFFER_OVERSIZE + dibitOffset);
updateScramblingCode(frameFragment);
mValidFragment = hasValidMacMessage(frameFragment);
broadcast(frameFragment);
}
@@ -269,11 +374,51 @@ public class P25P2SuperFrameDetector implements Listener<Dibit>, ISyncDetectList
//Xor the two messages to produce a unified super frame fragment
message1.xor(message2);
message1.setCorrectedBitCount(bitErrors); //Not even sure what the correct bit error count is here?
SuperFrameFragment frameFragment = new SuperFrameFragment(message1, getCurrentTimestamp(), mScramblingSequence);
//Soft symbols are similarly aligned to sync 1 before dibit 540 and to sync 2 from dibit 540 onward
float[] softSymbols = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + sync1Offset, 720);
float[] softSymbols2 = getSoftSymbols(FRAGMENT_BUFFER_OVERSIZE + sync2Offset, 720);
if(softSymbols != null)
{
System.arraycopy(softSymbols2, 540, softSymbols, 540, 180);
}
SuperFrameFragment frameFragment = new SuperFrameFragment(message1, softSymbols, getCurrentTimestamp(),
mScramblingSequence);
updateIschSequence(frameFragment, mDibitCounter - FRAGMENT_BUFFER_OVERSIZE + sync2Offset);
updateScramblingCode(frameFragment);
mValidFragment = hasValidMacMessage(frameFragment);
broadcast(frameFragment);
}
/**
* Tracks the ISCH sequence (ie fragment 1, 2 or 3 of the superframe) of fragments with a valid I-ISCH as an anchor
* and predicts the ISCH sequence for fragments where neither I-ISCH is valid, from the number of whole fragments
* since the anchor fragment. The ISCH sequence determines the scrambling sequence offset and the order of the
* final two timeslots in the fragment.
* @param fragment to update
* @param fragmentEnd dibit counter value for the final dibit of the fragment
*/
private void updateIschSequence(SuperFrameFragment fragment, long fragmentEnd)
{
if(fragment.hasDecodedIschSequence())
{
mAnchorDibitCounter = fragmentEnd;
mAnchorIschSequence = fragment.getIschSequence();
}
else if(mAnchorDibitCounter >= 0)
{
long elapsed = fragmentEnd - mAnchorDibitCounter;
long fragments = Math.round(elapsed / (double)FRAGMENT_DIBIT_LENGTH);
if(fragments >= 1 && fragments <= MAX_PREDICTION_FRAGMENT_COUNT &&
Math.abs(elapsed - fragments * FRAGMENT_DIBIT_LENGTH) <= MAX_PREDICTION_DIBIT_OFFSET)
{
fragment.setPredictedIschSequence(mAnchorIschSequence.advance((int)fragments));
}
}
}
/**
* Updates the scrambling code when we receive a network status broadcast with WACN, SYSTEM and NAC.
* @param superFrameFragment
@@ -316,6 +461,30 @@ public class P25P2SuperFrameDetector implements Listener<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)
{
broadcast(new SyncLossMessage(getCurrentTimestamp(), dibitsProcessed * 2, Protocol.APCO25_PHASE2));

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -57,6 +57,22 @@ public enum ISCHSequence
return mTimeslotOffset;
}
/**
* Sequence of the fragment that follows this fragment by the specified number of fragments in the repeating
* sequence of 3 fragments per superframe.
* @param fragmentCount number of fragments to advance
* @return advanced sequence or UNKNOWN if this is not a fragment (1-3) sequence.
*/
public ISCHSequence advance(int fragmentCount)
{
if(this == ISCH_1 || this == ISCH_2 || this == ISCH_3)
{
return fromValue((getValue() + fragmentCount) % 3);
}
return UNKNOWN;
}
/**
* Indicates if this sequence is a final fragment sequence
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase2.message;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.ReedSolomon_44_16_29_P25;
import io.github.dsheirer.edac.ReedSolomonErasureDecoder;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice4Timeslot;
@@ -42,6 +42,10 @@ import org.slf4j.LoggerFactory;
public class EncryptionSynchronizationSequenceProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(EncryptionSynchronizationSequenceProcessor.class);
/**
* RS(44,16,29) shortened from RS(63,35,29). The decoder has no mutable state.
*/
private static final ReedSolomonErasureDecoder REED_SOLOMON = ReedSolomonErasureDecoder.p25_63_35_29();
private BinaryMessage mESSA;
private BinaryMessage mESSB1;
@@ -114,70 +118,51 @@ public class EncryptionSynchronizationSequenceProcessor
public EncryptionSynchronizationSequence getSequence()
{
//We need a minimum of ESS-A and at least one of the ESS-B sequences before we can decode. The Reed Solomon
//code can correct up to 14 symbol errors across the 16 info symbols provided by ESS-B and the 28 parity symbols
//provided by ESS-A
//code can correct v symbol errors and e erasures, where 2v + e <= 28, across the 16 info symbols provided by
//ESS-B and the 28 parity symbols provided by ESS-A. The symbols for any missing ESS-B fragments are first
//decoded as zero valued symbols, which are correct for unencrypted calls (ie zero key ID and message
//indicator), and then as erasures, which supports encrypted calls.
if(mESSA != null && (mESSB1 != null || mESSB2 != null || mESSB3 != null || mESSB4 != null))
{
//We have to reverse the order of the information hexbits and the RS parity hexbits in the array for
//the RS algorithm.
int[] input = new int[63];
int inputPointer = 0;
int[] erasures = new int[12];
int erasureCount = 0;
for(int x = 27; x >= 0; x--)
{
input[inputPointer++] = mESSA.getInt(x * 6, x * 6 + 5);
input[27 - x] = mESSA.getInt(x * 6, x * 6 + 5);
}
if(mESSB4 != null)
{
for(int x = 3; x >= 0; x--)
{
input[inputPointer++] = mESSB4.getInt(x * 6, x * 6 + 5);
}
}
else
{
inputPointer += 4;
}
BinaryMessage[] essB = {mESSB4, mESSB3, mESSB2, mESSB1};
if(mESSB3 != null)
for(int fragment = 0; fragment < essB.length; fragment++)
{
for(int x = 3; x >= 0; x--)
{
input[inputPointer++] = mESSB3.getInt(x * 6, x * 6 + 5);
}
}
else
{
inputPointer += 4;
}
int inputPointer = 28 + (fragment * 4);
if(mESSB2 != null)
{
for(int x = 3; x >= 0; x--)
{
input[inputPointer++] = mESSB2.getInt(x * 6, x * 6 + 5);
}
}
else
{
inputPointer += 4;
}
if(essB[fragment] != null)
{
input[inputPointer] = essB[fragment].getInt(x * 6, x * 6 + 5);
}
else
{
erasures[erasureCount++] = inputPointer;
}
if(mESSB1 != null)
{
for(int x = 3; x >= 0; x--)
{
input[inputPointer++] = mESSB1.getInt(x * 6, x * 6 + 5);
inputPointer++;
}
}
int[] output = new int[63];
boolean irrecoverableErrors = decode(input, output, erasures, 0);
ReedSolomon_44_16_29_P25 rs = new ReedSolomon_44_16_29_P25();
boolean irrecoverableErrors = rs.decode(input, output);
if(irrecoverableErrors && erasureCount > 0)
{
irrecoverableErrors = decode(input, output, erasures, erasureCount);
}
if(!irrecoverableErrors)
{
@@ -188,11 +173,7 @@ public class EncryptionSynchronizationSequenceProcessor
for(int x = 43; x >= 28; x--)
{
if(output[x] != -1)
{
message.load(pointer, 6, output[x]);
}
message.load(pointer, 6, output[x]);
pointer += 6;
}
@@ -203,6 +184,33 @@ public class EncryptionSynchronizationSequenceProcessor
return null;
}
/**
* Decodes the RS(44,16,29) codeword.
* @param input codeword
* @param output for the corrected codeword
* @param erasures positions of the erased symbols
* @param erasureCount number of erasure positions to use
* @return true if the codeword has irrecoverable errors or a correction is located in a shortened symbol.
*/
private static boolean decode(int[] input, int[] output, int[] erasures, int erasureCount)
{
if(REED_SOLOMON.decode(input, output, erasures, erasureCount))
{
return true;
}
//The shortened symbols are known to be zero, so a correction in a shortened symbol is a decoding failure
for(int x = 44; x < output.length; x++)
{
if(output[x] != 0)
{
return true;
}
}
return false;
}
/**
* ESS-B fragment 1
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ISCHSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.isch.IISCH;
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
import io.github.dsheirer.module.decode.p25.phase2.message.isch.SISCH;
@@ -31,6 +32,7 @@ import io.github.dsheirer.module.decode.p25.phase2.timeslot.Timeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.TimeslotFactory;
import io.github.dsheirer.protocol.Protocol;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -56,6 +58,7 @@ public class SuperFrameFragment implements IMessage
private static final ISCHDecoder ISCH_DECODER = new ISCHDecoder();
private long mTimestamp;
private CorrectedBinaryMessage mMessage;
private float[] mSoftSymbols;
private IISCH mIISCH1;
private IISCH mIISCH2;
private SISCH mSISCH1;
@@ -66,6 +69,7 @@ public class SuperFrameFragment implements IMessage
private Timeslot mTimeslotD;
private List<Timeslot> mTimeslots;
private ScramblingSequence mScramblingSequence;
private ISCHSequence mPredictedIschSequence = ISCHSequence.UNKNOWN;
/**
@@ -75,8 +79,22 @@ public class SuperFrameFragment implements IMessage
* @param timestamp of the final bit of this fragment
*/
public SuperFrameFragment(CorrectedBinaryMessage message, long timestamp, ScramblingSequence scramblingSequence)
{
this(message, null, timestamp, scramblingSequence);
}
/**
* Constructs a fragment from the message and the soft symbols with the specified timestamp.
*
* @param message containing 1440-bit super-frame fragment (ie 1/3 of a superframe)
* @param softSymbols (radians) for each of the 720 dibits in the message, or null, for soft decision decoding
* @param timestamp of the final bit of this fragment
*/
public SuperFrameFragment(CorrectedBinaryMessage message, float[] softSymbols, long timestamp,
ScramblingSequence scramblingSequence)
{
mMessage = message;
mSoftSymbols = softSymbols;
mTimestamp = timestamp;
mScramblingSequence = scramblingSequence;
}
@@ -118,7 +136,6 @@ public class SuperFrameFragment implements IMessage
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
.getSubMessage(CHANNEL_A_ISCH_START, TIMESLOT_A_START), P25P2Message.TIMESLOT_1);
mIISCH1 = new IISCH(message, 0, 1, getTimestamp());
mIISCH1.setValid(message.getCorrectedBitCount() < 3);
}
return mIISCH1;
@@ -134,7 +151,6 @@ public class SuperFrameFragment implements IMessage
CorrectedBinaryMessage message = ISCH_DECODER.decode(getMessage()
.getSubMessage(CHANNEL_B_ISCH_START, TIMESLOT_B_START), P25P2Message.TIMESLOT_2);
mIISCH2 = new IISCH(message, 0, 2, getTimestamp());
mIISCH2.setValid(message.getCorrectedBitCount() < 3);
}
return mIISCH2;
@@ -309,7 +325,46 @@ public class SuperFrameFragment implements IMessage
{
CorrectedBinaryMessage message = getMessage().getSubMessage(start, end);
BinaryMessage timeslotSequence = mScramblingSequence.getTimeslotSequence(getTimeslotOffset() + index);
return TimeslotFactory.getTimeslot(message, timeslotSequence, timeslot, getTimestamp());
float[] softSymbols = mSoftSymbols != null ? Arrays.copyOfRange(mSoftSymbols, start / 2, end / 2) : null;
return TimeslotFactory.getTimeslot(message, softSymbols, timeslotSequence, timeslot, getTimestamp());
}
/**
* Sets the ISCH sequence predicted from the sequence of a preceding fragment, for use when neither of the I-ISCH
* in this fragment are valid. This must be set before accessing the timeslots.
* @param predicted sequence
*/
public void setPredictedIschSequence(ISCHSequence predicted)
{
mPredictedIschSequence = predicted;
}
/**
* Indicates if the ISCH sequence for this fragment was decoded from a valid I-ISCH.
*/
public boolean hasDecodedIschSequence()
{
return getIISCH1().isValid() || getIISCH2().isValid();
}
/**
* ISCH sequence (ie fragment 1, 2 or 3 of the superframe) decoded from a valid I-ISCH, or the predicted sequence
* when neither of the I-ISCH are valid.
* @return sequence or UNKNOWN
*/
public ISCHSequence getIschSequence()
{
if(getIISCH1().isValid())
{
return getIISCH1().getIschSequence();
}
if(getIISCH2().isValid())
{
return getIISCH2().getIschSequence();
}
return mPredictedIschSequence;
}
/**
@@ -317,37 +372,17 @@ public class SuperFrameFragment implements IMessage
*/
private int getTimeslotOffset()
{
if(getIISCH1().isValid())
{
return getIISCH1().getIschSequence().getTimeslotOffset();
}
if(getIISCH2().isValid())
{
return getIISCH2().getIschSequence().getTimeslotOffset();
}
return 0;
return getIschSequence().getTimeslotOffset();
}
/**
* Indicates if this fragment is the final fragment (3 of 3) for a superframe which indicates that the final two
* timeslots are inverted.
* timeslots are inverted. When the sequence can't be determined, the higher probability is that it's not the
* final fragment.
*/
private boolean isFinalFragment()
{
if(getIISCH1().isValid())
{
return getIISCH1().getIschSequence().isFinalFragment();
}
if(getIISCH2().isValid())
{
return getIISCH2().getIschSequence().isFinalFragment();
}
//Can't determine, so higher probability is that it's not the final fragment
return false;
return getIschSequence().isFinalFragment();
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -41,6 +41,7 @@ public class ISCHDecoder
private static final long[] GENERATOR = {0x8816CE36D7l, 0x201DFD4F64l, 0x100F4B1758l, 0x0C00DED18El, 0x020807F7FFl,
0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l};
private static final long CODE_WORD_OFFSET = 0x184229d461l;
private static final int MAX_CORRECTABLE_BIT_ERRORS = 7;
/**
* Constructs the ISCH-I parsing class
@@ -93,6 +94,46 @@ public class ISCHDecoder
return codeword;
}
/**
* Decodes the codeword to the nearest (minimum Hamming distance) valid codeword in the map. The (40,9,16) code
* has a minimum distance of 16, so it corrects up to 7 bit errors.
* @param codeword received
* @param codewordMap of valid codewords to messages
* @return decoded message with the corrected bit count, or null if no valid codeword is within 7 bit errors.
*/
private static CorrectedBinaryMessage decode(long codeword, Map<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
* only use the codewords that are correct for the expected timeslot.
@@ -105,64 +146,20 @@ public class ISCHDecoder
if(expectedTimeslot == 1)
{
if(CODEWORD_MAP_TS1.containsKey(codeword))
CorrectedBinaryMessage decoded = decode(codeword, CODEWORD_MAP_TS1);
if(decoded != null)
{
return new CorrectedBinaryMessage(CODEWORD_MAP_TS1.get(codeword));
}
else
{
int smallestErrorCount = 8;
long closestCodeword = 0;
for(long validCodeword: CODEWORD_MAP_TS1.keySet())
{
long mask = codeword & validCodeword;
int errorCount = Long.bitCount(mask);
if(errorCount < smallestErrorCount)
{
smallestErrorCount = errorCount;
closestCodeword = validCodeword;
}
}
if(closestCodeword != 0 && smallestErrorCount <= 7) //Max correctable bit errors of 7
{
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(CODEWORD_MAP_TS1.get(closestCodeword));
decoded.setCorrectedBitCount(smallestErrorCount);
return decoded;
}
return decoded;
}
}
else if(expectedTimeslot == 2)
{
if(CODEWORD_MAP_TS2.containsKey(codeword))
CorrectedBinaryMessage decoded = decode(codeword, CODEWORD_MAP_TS2);
if(decoded != null)
{
return new CorrectedBinaryMessage(CODEWORD_MAP_TS2.get(codeword));
}
else
{
int smallestErrorCount = 16;
long closestCodeword = 0;
for(long validCodeword: CODEWORD_MAP_TS2.keySet())
{
long mask = codeword & validCodeword;
int errorCount = Long.bitCount(mask);
if(errorCount < smallestErrorCount)
{
smallestErrorCount = errorCount;
closestCodeword = validCodeword;
}
}
if(closestCodeword != 0 && smallestErrorCount <= 7) //Max correctable bit errors of 7
{
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(CODEWORD_MAP_TS2.get(closestCodeword));
decoded.setCorrectedBitCount(smallestErrorCount);
return decoded;
}
return decoded;
}
}
else

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -162,6 +162,30 @@ public class MacMessageFactory
private final static Logger mLog = LoggerFactory.getLogger(MacMessageFactory.class);
/**
* Indicates if the message passes the CRC check for the data unit (FACCH, SACCH or LCCH).
* @param dataUnitID that identifies the message type
* @param message to check
* @return true if the CRC check passes or the data unit has no CRC.
*/
public static boolean passesCRC(DataUnitID dataUnitID, CorrectedBinaryMessage message)
{
if(dataUnitID.isFACCH())
{
return CRCP25.crc12_FACCH(message);
}
else if(dataUnitID.isLCCH())
{
return CRCP25.crc16_LCCH(message);
}
else if(dataUnitID.isSACCH())
{
return CRCP25.crc12_SACCH(message);
}
return true;
}
/**
* Creates a set of MAC messages
* @param timeslot for the messages
@@ -176,26 +200,13 @@ public class MacMessageFactory
{
List<MacMessage> messages = new ArrayList<>();
boolean passesCRC = true;
boolean passesCRC = passesCRC(dataUnitID, message);
int nac = 0;
if(dataUnitID.isFACCH())
if(passesCRC && dataUnitID.isLCCH())
{
passesCRC = CRCP25.crc12_FACCH(message);
}
else if(dataUnitID.isLCCH())
{
passesCRC = CRCP25.crc16_LCCH(message);
if(passesCRC)
{
nac = MacStructure.getLcchNac(message);
}
}
else if(dataUnitID.isSACCH())
{
passesCRC = CRCP25.crc12_SACCH(message);
nac = MacStructure.getLcchNac(message);
}
if(!passesCRC)

View File

@@ -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();
}
}

View File

@@ -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();
};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2026 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,15 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.ReedSolomonErasureDecoder;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -36,6 +41,38 @@ import java.util.List;
*/
public abstract class AbstractSignalingTimeslot extends Timeslot
{
/**
* RS(63,35,29) errors-and-erasures decoder shared by the signaling timeslots. The decoder has no mutable state.
*/
private static final ReedSolomonErasureDecoder REED_SOLOMON = ReedSolomonErasureDecoder.p25_63_35_29();
/**
* Index of the first message (information) symbol in the RS(63,35,29) codeword. Lower indices are parity symbols.
*/
private static final int FIRST_MESSAGE_INDEX = 28;
/**
* Soft decision decoding: number of additional (least reliable) received symbols erased for each decoding attempt
* after the punctured parity symbols-only attempt fails. Each step of 2 erasures reduces the error correction
* capacity by one symbol error.
*/
private static final int SOFT_ERASURE_STEP = 2;
/**
* Maximum total (punctured plus soft) erasures for soft decision decoding attempts. Each attempt is a chance for a
* miscorrection to pass the CRC check, and the miscorrection probability increases as the erasures consume the error
* correction capacity. Testing against Phase 2 recordings with injected noise (valid MAC messages at 9 dB in-band
* C/N) and against random (noise) timeslots (SACCH CRC-valid messages per timeslot):
* punctured erasures only: 3125, 0/200k
* 14: 3321, 0/200k
* 20: 3391, 1/200k
* 24: 8/200k
* 28: 3405, 91/200k
* FACCH (9 shortened symbols) produced no CRC-valid messages from random timeslots at any setting.
*/
private static final int SOFT_ERASURE_MAX = 20;
private static final float HALF_PI = (float)(Math.PI / 2.0);
private static final float PI = (float)Math.PI;
private static final float TWO_PI = (float)(2.0 * Math.PI);
private float[] mSoftSymbols;
/**
* Constructs a scrambled signaling timeslot
*
@@ -75,4 +112,224 @@ public abstract class AbstractSignalingTimeslot extends Timeslot
* Access Encoded MAC Information (EMI) message(s) carried by the signalling timeslot
*/
public abstract List<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;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,22 +23,15 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fast Associated Control CHannel (FACCH) timeslot carrying a S-OEMI Message
*/
public class FacchTimeslot extends AbstractSignalingTimeslot
{
private final static Logger mLog = LoggerFactory.getLogger(FacchTimeslot.class);
private static final int MAX_OCTET_INDEX = 144; //156-12 = message length minus CRC-12 checksum.
private static final IntField INFO_1 = IntField.range(2, 7);
@@ -138,7 +131,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
if(mMacMessages == null)
{
int[] input = new int[63];
int[] output = new int[63];
float[] reliabilities = createReliabilities();
// input[0] = 0; //Punctured
// input[1] = 0; //Punctured
@@ -149,51 +142,51 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
// input[6] = 0; //Punctured
// input[7] = 0; //Punctured
// input[8] = 0; //Punctured
input[9] = getInt(PARITY_19);
input[10] = getInt(PARITY_18);
input[11] = getInt(PARITY_17);
input[12] = getInt(PARITY_16);
input[13] = getInt(PARITY_15);
input[14] = getInt(PARITY_14);
input[15] = getInt(PARITY_13);
input[16] = getInt(PARITY_12);
input[17] = getInt(PARITY_11);
input[18] = getInt(PARITY_10);
input[19] = getInt(PARITY_9);
input[20] = getInt(PARITY_8);
input[21] = getInt(PARITY_7);
input[22] = getInt(PARITY_6);
input[23] = getInt(PARITY_5);
input[24] = getInt(PARITY_4);
input[25] = getInt(PARITY_3);
input[26] = getInt(PARITY_2);
input[27] = getInt(PARITY_1);
input[28] = getInt(INFO_26);
input[29] = getInt(INFO_25);
input[30] = getInt(INFO_24);
input[31] = getInt(INFO_23);
input[32] = getInt(INFO_22);
input[33] = getInt(INFO_21);
input[34] = getInt(INFO_20);
input[35] = getInt(INFO_19);
input[36] = getInt(INFO_18);
input[37] = getInt(INFO_17);
input[38] = getInt(INFO_16);
input[39] = getInt(INFO_15);
input[40] = getInt(INFO_14);
input[41] = getInt(INFO_13);
input[42] = getInt(INFO_12);
input[43] = getInt(INFO_11);
input[44] = getInt(INFO_10);
input[45] = getInt(INFO_9);
input[46] = getInt(INFO_8);
input[47] = getInt(INFO_7);
input[48] = getInt(INFO_6);
input[49] = getInt(INFO_5);
input[50] = getInt(INFO_4);
input[51] = getInt(INFO_3);
input[52] = getInt(INFO_2);
input[53] = getInt(INFO_1);
load(input, reliabilities, 9, PARITY_19);
load(input, reliabilities, 10, PARITY_18);
load(input, reliabilities, 11, PARITY_17);
load(input, reliabilities, 12, PARITY_16);
load(input, reliabilities, 13, PARITY_15);
load(input, reliabilities, 14, PARITY_14);
load(input, reliabilities, 15, PARITY_13);
load(input, reliabilities, 16, PARITY_12);
load(input, reliabilities, 17, PARITY_11);
load(input, reliabilities, 18, PARITY_10);
load(input, reliabilities, 19, PARITY_9);
load(input, reliabilities, 20, PARITY_8);
load(input, reliabilities, 21, PARITY_7);
load(input, reliabilities, 22, PARITY_6);
load(input, reliabilities, 23, PARITY_5);
load(input, reliabilities, 24, PARITY_4);
load(input, reliabilities, 25, PARITY_3);
load(input, reliabilities, 26, PARITY_2);
load(input, reliabilities, 27, PARITY_1);
load(input, reliabilities, 28, INFO_26);
load(input, reliabilities, 29, INFO_25);
load(input, reliabilities, 30, INFO_24);
load(input, reliabilities, 31, INFO_23);
load(input, reliabilities, 32, INFO_22);
load(input, reliabilities, 33, INFO_21);
load(input, reliabilities, 34, INFO_20);
load(input, reliabilities, 35, INFO_19);
load(input, reliabilities, 36, INFO_18);
load(input, reliabilities, 37, INFO_17);
load(input, reliabilities, 38, INFO_16);
load(input, reliabilities, 39, INFO_15);
load(input, reliabilities, 40, INFO_14);
load(input, reliabilities, 41, INFO_13);
load(input, reliabilities, 42, INFO_12);
load(input, reliabilities, 43, INFO_11);
load(input, reliabilities, 44, INFO_10);
load(input, reliabilities, 45, INFO_9);
load(input, reliabilities, 46, INFO_8);
load(input, reliabilities, 47, INFO_7);
load(input, reliabilities, 48, INFO_6);
load(input, reliabilities, 49, INFO_5);
load(input, reliabilities, 50, INFO_4);
load(input, reliabilities, 51, INFO_3);
load(input, reliabilities, 52, INFO_2);
load(input, reliabilities, 53, INFO_1);
// input[54] = 0; //Shortened
// input[55] = 0; //Shortened
// input[56] = 0; //Shortened
@@ -204,56 +197,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot
// input[61] = 0; //Shortened
// input[62] = 0; //Shortened
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
boolean irrecoverableErrors;
try
{
irrecoverableErrors = reedSolomon_63_35_29.decode(input, output);
}
catch(Exception e)
{
mLog.error("Error", e);
irrecoverableErrors = true;
}
CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(156);
int pointer = 0;
for(int x = 53; x >= 28; x--)
{
if(output[x] != -1)
{
binaryMessage.load(pointer, 6, output[x]);
}
pointer += 6;
}
if(irrecoverableErrors)
{
MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(),
new MacStructureFailedRS(binaryMessage, 0));
macMessage.setValid(false);
mMacMessages = new ArrayList<>();
mMacMessages.add(macMessage);
}
else
{
//If we corrected any bit errors, update the original message with the bit error count
for(int x = 9; x <= 53; x++)
{
if(output[x] != input[x])
{
binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x])));
}
}
}
mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(),
MAX_OCTET_INDEX);
mMacMessages = decode(input, reliabilities, 9, 53, MAX_OCTET_INDEX);
}
return mMacMessages;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,22 +21,15 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Logical Control CHannel (LCCH) timeslot carrying an I-OECI message
*/
public class LcchTimeslot extends AbstractSignalingTimeslot
{
private final static Logger mLog = LoggerFactory.getLogger(LcchTimeslot.class);
private static final int MAX_OCTET_INDEX = 152; //180-16-12 = message length minus CRC-16 checksum minus 12-bit NAC.
private static final IntField INFO_1 = IntField.range(2, 7);
@@ -125,7 +118,7 @@ public class LcchTimeslot extends AbstractSignalingTimeslot
if(mMacMessages == null)
{
int[] input = new int[63];
int[] output = new int[63];
float[] reliabilities = createReliabilities();
// input[0] = 0; //Punctured
// input[1] = 0; //Punctured
@@ -133,114 +126,65 @@ public class LcchTimeslot extends AbstractSignalingTimeslot
// input[3] = 0; //Punctured
// input[4] = 0; //Punctured
// input[5] = 0; //Punctured
input[6] = getInt(PARITY_22);
input[7] = getInt(PARITY_21);
input[8] = getInt(PARITY_20);
input[9] = getInt(PARITY_19);
input[10] = getInt(PARITY_18);
input[11] = getInt(PARITY_17);
input[12] = getInt(PARITY_16);
input[13] = getInt(PARITY_15);
input[14] = getInt(PARITY_14);
input[15] = getInt(PARITY_13);
input[16] = getInt(PARITY_12);
input[17] = getInt(PARITY_11);
input[18] = getInt(PARITY_10);
input[19] = getInt(PARITY_9);
input[20] = getInt(PARITY_8);
input[21] = getInt(PARITY_7);
input[22] = getInt(PARITY_6);
input[23] = getInt(PARITY_5);
input[24] = getInt(PARITY_4);
input[25] = getInt(PARITY_3);
input[26] = getInt(PARITY_2);
input[27] = getInt(PARITY_1);
input[28] = getInt(INFO_30);
input[29] = getInt(INFO_29);
input[30] = getInt(INFO_28);
input[31] = getInt(INFO_27);
input[32] = getInt(INFO_26);
input[33] = getInt(INFO_25);
input[34] = getInt(INFO_24);
input[35] = getInt(INFO_23);
input[36] = getInt(INFO_22);
input[37] = getInt(INFO_21);
input[38] = getInt(INFO_20);
input[39] = getInt(INFO_19);
input[40] = getInt(INFO_18);
input[41] = getInt(INFO_17);
input[42] = getInt(INFO_16);
input[43] = getInt(INFO_15);
input[44] = getInt(INFO_14);
input[45] = getInt(INFO_13);
input[46] = getInt(INFO_12);
input[47] = getInt(INFO_11);
input[48] = getInt(INFO_10);
input[49] = getInt(INFO_9);
input[50] = getInt(INFO_8);
input[51] = getInt(INFO_7);
input[52] = getInt(INFO_6);
input[53] = getInt(INFO_5);
input[54] = getInt(INFO_4);
input[55] = getInt(INFO_3);
input[56] = getInt(INFO_2);
input[57] = getInt(INFO_1);
load(input, reliabilities, 6, PARITY_22);
load(input, reliabilities, 7, PARITY_21);
load(input, reliabilities, 8, PARITY_20);
load(input, reliabilities, 9, PARITY_19);
load(input, reliabilities, 10, PARITY_18);
load(input, reliabilities, 11, PARITY_17);
load(input, reliabilities, 12, PARITY_16);
load(input, reliabilities, 13, PARITY_15);
load(input, reliabilities, 14, PARITY_14);
load(input, reliabilities, 15, PARITY_13);
load(input, reliabilities, 16, PARITY_12);
load(input, reliabilities, 17, PARITY_11);
load(input, reliabilities, 18, PARITY_10);
load(input, reliabilities, 19, PARITY_9);
load(input, reliabilities, 20, PARITY_8);
load(input, reliabilities, 21, PARITY_7);
load(input, reliabilities, 22, PARITY_6);
load(input, reliabilities, 23, PARITY_5);
load(input, reliabilities, 24, PARITY_4);
load(input, reliabilities, 25, PARITY_3);
load(input, reliabilities, 26, PARITY_2);
load(input, reliabilities, 27, PARITY_1);
load(input, reliabilities, 28, INFO_30);
load(input, reliabilities, 29, INFO_29);
load(input, reliabilities, 30, INFO_28);
load(input, reliabilities, 31, INFO_27);
load(input, reliabilities, 32, INFO_26);
load(input, reliabilities, 33, INFO_25);
load(input, reliabilities, 34, INFO_24);
load(input, reliabilities, 35, INFO_23);
load(input, reliabilities, 36, INFO_22);
load(input, reliabilities, 37, INFO_21);
load(input, reliabilities, 38, INFO_20);
load(input, reliabilities, 39, INFO_19);
load(input, reliabilities, 40, INFO_18);
load(input, reliabilities, 41, INFO_17);
load(input, reliabilities, 42, INFO_16);
load(input, reliabilities, 43, INFO_15);
load(input, reliabilities, 44, INFO_14);
load(input, reliabilities, 45, INFO_13);
load(input, reliabilities, 46, INFO_12);
load(input, reliabilities, 47, INFO_11);
load(input, reliabilities, 48, INFO_10);
load(input, reliabilities, 49, INFO_9);
load(input, reliabilities, 50, INFO_8);
load(input, reliabilities, 51, INFO_7);
load(input, reliabilities, 52, INFO_6);
load(input, reliabilities, 53, INFO_5);
load(input, reliabilities, 54, INFO_4);
load(input, reliabilities, 55, INFO_3);
load(input, reliabilities, 56, INFO_2);
load(input, reliabilities, 57, INFO_1);
// input[58] = 0; //Shortened
// input[59] = 0; //Shortened
// input[60] = 0; //Shortened
// input[61] = 0; //Shortened
// input[62] = 0; //Shortened
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
boolean irrecoverableErrors;
try
{
irrecoverableErrors = reedSolomon_63_35_29.decode(input, output);
}
catch(Exception e)
{
mLog.error("Error", e);
irrecoverableErrors = true;
}
CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(180);
int pointer = 0;
for(int x = 57; x >= 28; x--)
{
if(output[x] != -1)
{
binaryMessage.load(pointer, 6, output[x]);
}
pointer += 6;
}
if(irrecoverableErrors)
{
MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(),
new MacStructureFailedRS(binaryMessage, 0));
macMessage.setValid(false);
mMacMessages = new ArrayList<>();
mMacMessages.add(macMessage);
}
else
{
//If we corrected any bit errors, update the original message with the bit error count
for(int x = 6; x <= 57; x++)
{
if(output[x] != input[x])
{
binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x])));
}
}
mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(),
MAX_OCTET_INDEX);
}
mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX);
}
return mMacMessages;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,12 +22,8 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureFailedRS;
import java.util.ArrayList;
import java.util.List;
/**
@@ -148,7 +144,7 @@ public class SacchTimeslot extends AbstractSignalingTimeslot
if(mMacMessages == null)
{
int[] input = new int[63];
int[] output = new int[63];
float[] reliabilities = createReliabilities();
// input[0] = 0; //Punctured
// input[1] = 0; //Punctured
@@ -156,115 +152,65 @@ public class SacchTimeslot extends AbstractSignalingTimeslot
// input[3] = 0; //Punctured
// input[4] = 0; //Punctured
// input[5] = 0; //Punctured
input[6] = getInt(PARITY_22);
input[7] = getInt(PARITY_21);
input[8] = getInt(PARITY_20);
input[9] = getInt(PARITY_19);
input[10] = getInt(PARITY_18);
input[11] = getInt(PARITY_17);
input[12] = getInt(PARITY_16);
input[13] = getInt(PARITY_15);
input[14] = getInt(PARITY_14);
input[15] = getInt(PARITY_13);
input[16] = getInt(PARITY_12);
input[17] = getInt(PARITY_11);
input[18] = getInt(PARITY_10);
input[19] = getInt(PARITY_9);
input[20] = getInt(PARITY_8);
input[21] = getInt(PARITY_7);
input[22] = getInt(PARITY_6);
input[23] = getInt(PARITY_5);
input[24] = getInt(PARITY_4);
input[25] = getInt(PARITY_3);
input[26] = getInt(PARITY_2);
input[27] = getInt(PARITY_1);
input[28] = getInt(INFO_30);
input[29] = getInt(INFO_29);
input[30] = getInt(INFO_28);
input[31] = getInt(INFO_27);
input[32] = getInt(INFO_26);
input[33] = getInt(INFO_25);
input[34] = getInt(INFO_24);
input[35] = getInt(INFO_23);
input[36] = getInt(INFO_22);
input[37] = getInt(INFO_21);
input[38] = getInt(INFO_20);
input[39] = getInt(INFO_19);
input[40] = getInt(INFO_18);
input[41] = getInt(INFO_17);
input[42] = getInt(INFO_16);
input[43] = getInt(INFO_15);
input[44] = getInt(INFO_14);
input[45] = getInt(INFO_13);
input[46] = getInt(INFO_12);
input[47] = getInt(INFO_11);
input[48] = getInt(INFO_10);
input[49] = getInt(INFO_9);
input[50] = getInt(INFO_8);
input[51] = getInt(INFO_7);
input[52] = getInt(INFO_6);
input[53] = getInt(INFO_5);
input[54] = getInt(INFO_4);
input[55] = getInt(INFO_3);
input[56] = getInt(INFO_2);
input[57] = getInt(INFO_1);
load(input, reliabilities, 6, PARITY_22);
load(input, reliabilities, 7, PARITY_21);
load(input, reliabilities, 8, PARITY_20);
load(input, reliabilities, 9, PARITY_19);
load(input, reliabilities, 10, PARITY_18);
load(input, reliabilities, 11, PARITY_17);
load(input, reliabilities, 12, PARITY_16);
load(input, reliabilities, 13, PARITY_15);
load(input, reliabilities, 14, PARITY_14);
load(input, reliabilities, 15, PARITY_13);
load(input, reliabilities, 16, PARITY_12);
load(input, reliabilities, 17, PARITY_11);
load(input, reliabilities, 18, PARITY_10);
load(input, reliabilities, 19, PARITY_9);
load(input, reliabilities, 20, PARITY_8);
load(input, reliabilities, 21, PARITY_7);
load(input, reliabilities, 22, PARITY_6);
load(input, reliabilities, 23, PARITY_5);
load(input, reliabilities, 24, PARITY_4);
load(input, reliabilities, 25, PARITY_3);
load(input, reliabilities, 26, PARITY_2);
load(input, reliabilities, 27, PARITY_1);
load(input, reliabilities, 28, INFO_30);
load(input, reliabilities, 29, INFO_29);
load(input, reliabilities, 30, INFO_28);
load(input, reliabilities, 31, INFO_27);
load(input, reliabilities, 32, INFO_26);
load(input, reliabilities, 33, INFO_25);
load(input, reliabilities, 34, INFO_24);
load(input, reliabilities, 35, INFO_23);
load(input, reliabilities, 36, INFO_22);
load(input, reliabilities, 37, INFO_21);
load(input, reliabilities, 38, INFO_20);
load(input, reliabilities, 39, INFO_19);
load(input, reliabilities, 40, INFO_18);
load(input, reliabilities, 41, INFO_17);
load(input, reliabilities, 42, INFO_16);
load(input, reliabilities, 43, INFO_15);
load(input, reliabilities, 44, INFO_14);
load(input, reliabilities, 45, INFO_13);
load(input, reliabilities, 46, INFO_12);
load(input, reliabilities, 47, INFO_11);
load(input, reliabilities, 48, INFO_10);
load(input, reliabilities, 49, INFO_9);
load(input, reliabilities, 50, INFO_8);
load(input, reliabilities, 51, INFO_7);
load(input, reliabilities, 52, INFO_6);
load(input, reliabilities, 53, INFO_5);
load(input, reliabilities, 54, INFO_4);
load(input, reliabilities, 55, INFO_3);
load(input, reliabilities, 56, INFO_2);
load(input, reliabilities, 57, INFO_1);
// input[58] = 0; //Shortened
// input[59] = 0; //Shortened
// input[60] = 0; //Shortened
// input[61] = 0; //Shortened
// input[62] = 0; //Shortened
ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25();
boolean irrecoverableErrors;
try
{
irrecoverableErrors = reedSolomon_63_35_29.decode(input, output);
}
catch(Exception e)
{
e.printStackTrace();
irrecoverableErrors = true;
}
CorrectedBinaryMessage binaryMessage = new CorrectedBinaryMessage(180);
int pointer = 0;
for(int x = 57; x >= 28; x--)
{
if(output[x] != -1)
{
binaryMessage.load(pointer, 6, output[x]);
}
pointer += 6;
}
if(irrecoverableErrors)
{
MacMessage macMessage = new MacMessage(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(),
new MacStructureFailedRS(binaryMessage, 0));
macMessage.setValid(false);
mMacMessages = new ArrayList<>();
mMacMessages.add(macMessage);
}
else
{
//If we corrected any bit errors, update the original message with the bit error count
for(int x = 6; x <= 57; x++)
{
if(output[x] != input[x])
{
binaryMessage.incrementCorrectedBitCount(Integer.bitCount((output[x] ^ input[x])));
}
}
mMacMessages = MacMessageFactory.create(getTimeslot(), getDataUnitID(), binaryMessage, getTimestamp(), 99);
}
mMacMessages = decode(input, reliabilities, 6, 57, MAX_OCTET_INDEX);
}
return mMacMessages;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -39,6 +39,35 @@ public class TimeslotFactory
*/
public static Timeslot getTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence,
int timeslot, long timestamp)
{
return getTimeslot(message, null, scramblingSequence, timeslot, timestamp);
}
/**
* Creates a timeslot parser instance with soft symbols for soft decision decoding of signaling timeslots.
*
* @param message containing 320-bit timeslot data
* @param softSymbols (radians) for each of the 160 dibits in the timeslot, or null
* @param scramblingSequence to unscramble the transmitted message
* @param timeslot to retrieve
* @param timestamp for the message
* @return timeslot parser
*/
public static Timeslot getTimeslot(CorrectedBinaryMessage message, float[] softSymbols,
BinaryMessage scramblingSequence, int timeslot, long timestamp)
{
Timeslot instance = createTimeslot(message, scramblingSequence, timeslot, timestamp);
if(softSymbols != null && instance instanceof AbstractSignalingTimeslot signalingTimeslot)
{
signalingTimeslot.setSoftSymbols(softSymbols);
}
return instance;
}
private static Timeslot createTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence,
int timeslot, long timestamp)
{
DataUnitID dataUnitID = Timeslot.getDuid(message);