#1067 P25 Phase 1 C4FM/LSM decoder redesign. (#2294)

C4FM now uses SIMD differential decoding and soft sync detection.
LSM updated with soft sync detector and retains original Gardner error detector.
Adds new QPSK symbol view in the now playing channel tab.
Restores PLL error driven tuner Auto-PPM feature.
Deletes the demod viewer GUI application and all instrumented decoders
Updates DMR to send soft symbols to new symbol view in Now Playing.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2025-11-16 06:52:17 -05:00
committed by GitHub
parent 48202da9e4
commit 0d886c272e
149 changed files with 5990 additions and 8984 deletions

View File

@@ -177,9 +177,9 @@ public class ProcessingChain implements Listener<ChannelEvent>
{
for(Module module : mModules)
{
if(module instanceof PrimaryDecoder primaryDecoder)
if(module instanceof PrimaryDecoder decoder)
{
return primaryDecoder;
return decoder;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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;
@@ -30,9 +27,16 @@ import io.github.dsheirer.source.SourceEvent;
* Decoder that provides feedback to signal source(s) via SourceEvent broadcasts and supports registering source
* event listeners.
*/
public abstract class FeedbackDecoder extends Decoder implements ISourceEventProvider
public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceEventProvider
{
private Listener<SourceEvent> mSourceEventListener;
private Listener<Float> mSymbolListener;
/**
* Protocol description suitable for display in the user interface
* @return description
*/
public abstract String getProtocolDescription();
/**
* Registers the listener to receive source events from this decoder
@@ -62,4 +66,43 @@ public abstract class FeedbackDecoder extends Decoder implements ISourceEventPro
mSourceEventListener.receive(sourceEvent);
}
}
/**
* Processes the current phase-locked loop (PLL) measurement and sends to the parent tuner for self correction.
* @param pllError as measured in the decoder.
*/
public void processPLLError(float pllError, int baudRate)
{
long frequencyError = (long)(baudRate / 2.0 * (pllError / Math.PI));
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(frequencyError, "Decoder measured error sync locked"));
}
/**
* Registers the listener to receive demodulated symbols.
* @param listener to receive symbols.
*/
public void setSymbolListener(Listener<Float> listener)
{
mSymbolListener = listener;
}
/**
* Removes any registered symbol listener
*/
public void removeSymbolListener()
{
mSymbolListener = null;
}
/**
* Broadcasts the demodulated symbol to a symbol listener.
* @param symbol in range 0 to +/- PI radians
*/
public void broadcast(float symbol)
{
if(mSymbolListener != null)
{
mSymbolListener.receive(symbol);
}
}
}

View File

@@ -1,71 +1,27 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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;
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.sample.Listener;
/**
* Primary decoder adds the following functionality over the basic decoder:
*
* - Provides audio squelch control
* Base class for primary decoder
*/
public abstract class PrimaryDecoder extends Decoder implements ISquelchStateProvider
public abstract class PrimaryDecoder extends Decoder
{
protected Listener<SquelchStateEvent> mSquelchStateListener;
protected DecodeConfiguration mDecodeConfiguration;
public PrimaryDecoder(DecodeConfiguration config)
{
mDecodeConfiguration = config;
}
public DecodeConfiguration getDecodeConfiguration()
{
return mDecodeConfiguration;
}
public void broadcast(SquelchStateEvent state)
{
if(mSquelchStateListener != null)
{
mSquelchStateListener.receive(state);
}
}
@Override
public void setSquelchStateListener(Listener<SquelchStateEvent> listener)
{
mSquelchStateListener = listener;
}
@Override
public void removeSquelchStateListener()
{
mSquelchStateListener = null;
}
}

View File

@@ -0,0 +1,71 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode;
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.sample.Listener;
/**
* Primary decoder that adds the following functionality over the basic decoder:
*
* - Provides audio squelch control
*/
public abstract class SquelchControlDecoder extends PrimaryDecoder implements ISquelchStateProvider
{
protected Listener<SquelchStateEvent> mSquelchStateListener;
protected DecodeConfiguration mDecodeConfiguration;
/**
* Constructs an instance
* @param config for the decoder
*/
public SquelchControlDecoder(DecodeConfiguration config)
{
mDecodeConfiguration = config;
}
public DecodeConfiguration getDecodeConfiguration()
{
return mDecodeConfiguration;
}
public void broadcast(SquelchStateEvent state)
{
if(mSquelchStateListener != null)
{
mSquelchStateListener.receive(state);
}
}
@Override
public void setSquelchStateListener(Listener<SquelchStateEvent> listener)
{
mSquelchStateListener = listener;
}
@Override
public void removeSquelchStateListener()
{
mSquelchStateListener = null;
}
}

View File

@@ -36,7 +36,7 @@ import io.github.dsheirer.dsp.squelch.NoiseSquelch;
import io.github.dsheirer.dsp.squelch.NoiseSquelchState;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.module.decode.SquelchControlDecoder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
@@ -50,7 +50,7 @@ import org.slf4j.LoggerFactory;
/**
* AM decoder module with integrated squelch control
*/
public class AMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
public class AMDecoder extends SquelchControlDecoder implements ISourceEventListener, ISourceEventProvider,
IComplexSamplesListener, Listener<ComplexSamples>, IRealBufferProvider,
IDecoderStateEventProvider, INoiseSquelchController
{

View File

@@ -26,14 +26,14 @@ import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulator;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.carrier.CarrierOffsetProcessor;
import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
@@ -82,7 +82,7 @@ import org.slf4j.LoggerFactory;
* The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and
* embedded link control parameters.
*/
public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
ISourceEventProvider, Listener<ComplexSamples>
{
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
@@ -90,7 +90,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
private static final int SYMBOL_RATE = 4800;
private static final float MAXIMUM_CARRIER_OFFSET = 5000.0f; //Threshold for retuning the signal.
private static final Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
private DifferentialDemodulator mDemodulator;
private DifferentialDemodulatorFloat mDemodulator;
private final DMRMessageFramer mMessageFramer;
private final DMRSoftSymbolProcessor mSymbolProcessor;
private final DMRMessageProcessor mMessageProcessor;
@@ -111,7 +111,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
{
DMRCrcMaskManager crcMaskManager = new DMRCrcMaskManager(config.getIgnoreCRCChecksums());
mMessageFramer = new DMRMessageFramer(crcMaskManager);
mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer);
mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer, this);
mMessageProcessor = new DMRMessageProcessor(config, crcMaskManager);
mMessageProcessor.setMessageListener(getMessageListener());
setSampleRate(25000.0);
@@ -171,7 +171,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
mRRCFilterI = new RealFIRFilter(taps);
mRRCFilterQ = new RealFIRFilter(taps);
mDemodulator = DifferentialDemodulatorFactory.getDemodulator(decimatedSampleRate, SYMBOL_RATE);
mDemodulator = DifferentialDemodulatorFactory.getFloatDemodulator(decimatedSampleRate, SYMBOL_RATE);
mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol());
mMessageFramer.setListener(mMessageProcessor);
mMessageProcessor.setMessageListener(getMessageListener());
@@ -293,6 +293,12 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
return this::process;
}
@Override
public String getProtocolDescription()
{
return "DMR C4FM";
}
/**
* Sets the source event listener to receive source events from this decoder.
*/

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitDelayLine;
import io.github.dsheirer.module.decode.dmr.sync.DMRHardSyncDetector;
import java.nio.ByteBuffer;

View File

@@ -21,13 +21,15 @@ package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitDelayLine;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.gui.viewer.sync.SyncResultsViewer;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetector;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetectorFactory;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncDetectMode;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncModeMonitor;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.sync.visualizer.SyncResultsViewer;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -65,9 +67,10 @@ public class DMRSoftSymbolProcessor
private static final int BUFFER_PROTECTED_REGION_DIBITS = 92;
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS;
private static final float EQUALIZER_LOOP_GAIN = 0.3f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 4.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.35f;
private static final int SYMBOL_RATE = 4800;
private static final float EQUALIZER_LOOP_GAIN = 0.15f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
private static final float MAXIMUM_POSITIVE_SAMPLE_PHASE = 3.5f;
private static final float MAXIMUM_NEGATIVE_SAMPLE_PHASE = -3.5f;
private static final float SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION = 0.005f; //.5%
@@ -100,12 +103,13 @@ public class DMRSoftSymbolProcessor
private int mBufferWorkspaceLength;
private int mSymbolsSinceLastSync = 0;
private SyncResultsViewer mSyncResultsViewer;
private FeedbackDecoder mFeedbackDecoder;
/**
* Constructs an instance
* @param messageFramer to receive demodulated symbols/dibits and sync detect notifications
*/
public DMRSoftSymbolProcessor(DMRMessageFramer messageFramer)
public DMRSoftSymbolProcessor(DMRMessageFramer messageFramer, FeedbackDecoder feedbackDecoder)
{
if(messageFramer == null)
{
@@ -113,6 +117,7 @@ public class DMRSoftSymbolProcessor
}
mMessageFramer = messageFramer;
mFeedbackDecoder = feedbackDecoder;
mSyncModeMonitor.add(mSyncDetector);
mSyncModeMonitor.add(mSyncDetectorSecondary);
}
@@ -215,6 +220,7 @@ public class DMRSoftSymbolProcessor
}
softSymbol = LinearInterpolator.calculate(mBuffer[mBufferPointer], mBuffer[mBufferPointer + 1], mSamplePoint);
mFeedbackDecoder.broadcast(softSymbol);
mSyncDetector.process(softSymbol);
Dibit symbol = toSymbol(softSymbol);
@@ -559,6 +565,8 @@ public class DMRSoftSymbolProcessor
mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE);
mFeedbackDecoder.processPLLError(mEqualizerBalance, SYMBOL_RATE);
//Constrain gain between 1.0f and 1.35f
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);
mEqualizerGain = Math.max(mEqualizerGain, 1.0f);
@@ -608,7 +616,8 @@ public class DMRSoftSymbolProcessor
Arrays.fill(sync, -1.57f); //Cutoff between -1 & -1 to show symbol timings
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, sync, samples, intervals, "BUFFER CONTENTS", countDownLatch);
mSyncResultsViewer.receive(symbols, sync, samples, intervals, mEqualizerBalance, mEqualizerGain,
"BUFFER CONTENTS", countDownLatch);
try
{
@@ -665,7 +674,7 @@ public class DMRSoftSymbolProcessor
}
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, pattern.toSymbols(), samples, intervals,
mSyncResultsViewer.receive(symbols, pattern.toSymbols(), samples, intervals, mEqualizerBalance, mEqualizerGain,
pattern + " Score: " + score + (primary ? " PRIMARY" : " SECONDARY"), countDownLatch);
try

View File

@@ -1,97 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Arrays;
/**
* Implements a circular buffer delay line for dibits.
*/
public class DibitDelayLine
{
private Dibit[] mDelayLine;
public int mPointer;
private int mLength;
/**
* Constructs an instance
* @param length of the delay line in dibits.
*/
public DibitDelayLine(int length)
{
mLength = length;
mDelayLine = new Dibit[length];
Arrays.fill(mDelayLine, Dibit.D00_PLUS_1);
}
/**
* Inserts the dibit into the delay line and returns the oldest dibit from the delay line.
* @param dibit to insert
* @return oldest dibit
*/
public Dibit insert(Dibit dibit)
{
Dibit ejected = mDelayLine[mPointer];
mDelayLine[mPointer++] = dibit;
mPointer %= mLength;
return ejected;
}
/**
* Overwrites the most recently added dibits in the delay line with the provided sequence. This can be used when
* a sync pattern is detected to overwrite the calculated symbols with the known sync pattern.
* @param dibits to overwrite
*/
public void update(Dibit[] dibits)
{
mPointer = mPointer - dibits.length;
if(mPointer < 0)
{
mPointer += mDelayLine.length;
}
for(Dibit dibit: dibits)
{
mDelayLine[mPointer++] = dibit;
mPointer %= mLength;
}
}
public void log()
{
StringBuilder sb = new StringBuilder();
int pointer = mPointer;
for(int i = 0; i < mDelayLine.length; i++)
{
Dibit dibit = mDelayLine[pointer];
sb.append(dibit.getBit1() ? "1" : "0");
sb.append(dibit.getBit2() ? "1" : "0");
sb.append(" ");
pointer++;
pointer = pointer % mLength;
}
System.out.println(sb);
}
}

View File

@@ -1,97 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.sync.visualizer;
import io.github.dsheirer.util.SwingUtils;
import java.util.concurrent.CountDownLatch;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javax.swing.JFrame;
/**
* Utility for viewing sync detection results
*/
public class SyncResultsViewer implements ISyncResultsListener
{
private ISyncResultsListener mListener;
public SyncResultsViewer()
{
initUI();
}
/**
* View sync detection results.
* @param symbols that were detected
* @param samples for the symbols
* @param sync that was detected
* @param syncIntervals symbol timing interval pointers into the I/Q sample arrays
* @param label to display to the user
* @param release latch to release via the UI when done viewing these results
*/
@Override
public void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, String label, CountDownLatch release)
{
if(mListener != null)
{
mListener.receive(symbols, samples, sync, syncIntervals, label, release);
}
}
/**
* Initializes the viewer UI
*/
private void initUI()
{
CountDownLatch latch = new CountDownLatch(1);
final JFXPanel fxPanel = new JFXPanel();
Platform.runLater(() -> {
Platform.setImplicitExit(false);
SyncVisualizer syncVisualizer = new SyncVisualizer();
mListener = syncVisualizer;
Scene scene = new Scene(syncVisualizer, 1400, 1000);
fxPanel.setScene(scene);
fxPanel.setVisible(true);
JFrame frame = new JFrame();
frame.setTitle("Sync Results Viewer");
frame.setContentPane(fxPanel);
frame.setSize(1400, 1000);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
SwingUtils.run(() -> {
frame.setVisible(true);
latch.countDown();
});
});
try
{
latch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
}

View File

@@ -1,144 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.sync.visualizer;
import java.util.concurrent.CountDownLatch;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
/**
* JavaFX viewer application for visualizing sample buffers and detected sync patterns
*/
public class SyncVisualizer extends VBox implements ISyncResultsListener
{
private static final float SYMBOL = (float)(Math.PI / 4.0);
private final NumberAxis mConstellationI = new NumberAxis();
private final NumberAxis mConstellationQ = new NumberAxis();
private final NumberAxis mSampleTiming = new NumberAxis();
private final NumberAxis mSamplePhase = new NumberAxis();
private final ScatterChart<Number,Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
private final LineChart<Number,Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
private final Button mReleaseButton = new Button("Next Sync");
private final Label mPatternLabel = new Label();
private CountDownLatch mRelease;
/**
* Constructs an instance
*/
public SyncVisualizer()
{
setPadding(new Insets(5));
mConstellationI.setLabel("Inphase");
mConstellationQ.setLabel("Quadrature");
mSampleTiming.setLabel("Sample Timing");
mSamplePhase.setLabel("Sample Phase (+/- PI)");
mReleaseButton.setOnAction(e ->
{
if(mRelease != null)
{
mRelease.countDown();
mReleaseButton.setDisable(true);
}
});
mConstellationChart.setPrefSize(400, 400);
HBox hbox = new HBox();
hbox.getChildren().add(mConstellationChart);
// VBox.setVgrow(hbox, Priority.ALWAYS);
VBox.setVgrow(mSampleChart, Priority.ALWAYS);
HBox buttons = new HBox();
buttons.setAlignment(Pos.BASELINE_CENTER);
buttons.setSpacing(10);
buttons.getChildren().addAll(mReleaseButton, mPatternLabel);
VBox.setVgrow(buttons, Priority.NEVER);
getChildren().addAll(hbox, mSampleChart, buttons);
}
@Override
public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, String label, CountDownLatch latch)
{
mRelease = latch;
mReleaseButton.setDisable(latch == null);
Platform.runLater(() -> {
XYChart.Series<Number,Number> constellationIdeal = new XYChart.Series<>();
constellationIdeal.setName("Constellation Ideal");
float PI_4 = (float)(Math.PI / 4);
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, -PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, -PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(1, 0));
constellationIdeal.getData().add(new XYChart.Data<>(-1, 0));
constellationIdeal.getData().add(new XYChart.Data<>(0, 1));
constellationIdeal.getData().add(new XYChart.Data<>(0, -1));
mConstellationChart.getData().clear();
mConstellationChart.getData().add(constellationIdeal);
XYChart.Series<Number,Number> constellation = new XYChart.Series<>();
constellation.setName("Constellation");
for(int x = 0; x < symbols.length; x++)
{
constellation.getData().add(new XYChart.Data<>(Math.cos(symbols[x]), Math.sin(symbols[x])));
}
mConstellationChart.getData().add(constellation);
XYChart.Series<Number,Number> sampleSeries = new XYChart.Series<>();
sampleSeries.setName("Demodulated Samples");
for(int x = 0; x < samples.length; x++)
{
sampleSeries.getData().add(new XYChart.Data<>(x, samples[x]));
}
mSampleChart.getData().clear();
mSampleChart.getData().add(sampleSeries);
XYChart.Series<Number,Number> patternSeries = new XYChart.Series<>();
patternSeries.setName("Ideal Sync Pattern");
for(int x = 0; x < sync.length; x++)
{
patternSeries.getData().add(new XYChart.Data<>(syncIntervals[x], sync[x]));
}
mSampleChart.getData().add(patternSeries);
mPatternLabel.setText(label);
});
}
}

View File

@@ -1,55 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.fleetsync2;
import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented;
import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX
*/
public class Fleetsync2DecoderInstrumented extends Fleetsync2Decoder implements IInstrumentedAFSK1200Decoder
{
public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty();
public Fleetsync2DecoderInstrumented()
{
super(new AFSK1200DecoderInstrumented(false));
}
@Override
public SimpleIntegerProperty getBufferCountProperty()
{
return bufferCount;
}
public AFSK1200DecoderInstrumented getAFSK1200Decoder()
{
return (AFSK1200DecoderInstrumented)getDecoder();
}
@Override
public void receive(float[] realBuffer)
{
super.receive(realBuffer);
bufferCount.setValue(bufferCount.intValue() + 1);
}
}

View File

@@ -1,55 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.lj1200;
import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented;
import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX
*/
public class LJ1200DecoderInstrumented extends LJ1200Decoder implements IInstrumentedAFSK1200Decoder
{
public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty();
public LJ1200DecoderInstrumented()
{
super(new AFSK1200DecoderInstrumented(false));
}
@Override
public SimpleIntegerProperty getBufferCountProperty()
{
return bufferCount;
}
public AFSK1200DecoderInstrumented getAFSK1200Decoder()
{
return (AFSK1200DecoderInstrumented)getDecoder();
}
@Override
public void receive(float[] realBuffer)
{
super.receive(realBuffer);
bufferCount.setValue(bufferCount.intValue() + 1);
}
}

View File

@@ -1,55 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.mdc1200;
import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented;
import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX
*/
public class MDCDecoderInstrumented extends MDCDecoder implements IInstrumentedAFSK1200Decoder
{
public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty();
public MDCDecoderInstrumented()
{
super(new AFSK1200DecoderInstrumented(false));
}
@Override
public SimpleIntegerProperty getBufferCountProperty()
{
return bufferCount;
}
public AFSK1200DecoderInstrumented getAFSK1200Decoder()
{
return (AFSK1200DecoderInstrumented)getDecoder();
}
@Override
public void receive(float[] realBuffer)
{
super.receive(realBuffer);
bufferCount.setValue(bufferCount.intValue() + 1);
}
}

View File

@@ -1,55 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.mpt1327;
import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented;
import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX
*/
public class MPT1327DecoderInstrumented extends MPT1327Decoder implements IInstrumentedAFSK1200Decoder
{
public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty();
public MPT1327DecoderInstrumented()
{
super(new AFSK1200DecoderInstrumented(true), Sync.NORMAL);
}
@Override
public SimpleIntegerProperty getBufferCountProperty()
{
return bufferCount;
}
public AFSK1200DecoderInstrumented getAFSK1200Decoder()
{
return (AFSK1200DecoderInstrumented)getDecoder();
}
@Override
public void receive(float[] realBuffer)
{
super.receive(realBuffer);
bufferCount.setValue(bufferCount.intValue() + 1);
}
}

View File

@@ -36,7 +36,7 @@ import io.github.dsheirer.dsp.squelch.NoiseSquelch;
import io.github.dsheirer.dsp.squelch.NoiseSquelchState;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.module.decode.SquelchControlDecoder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
@@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory;
* audio to Noise Squelch. Squelch operates on the noise level with open and close thresholds to pass low-noise audio
* and block high-noise audio. Audio is filtered and resampled to 8 kHz for downstream consumers.
*/
public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, IComplexSamplesListener,
public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventListener, IComplexSamplesListener,
Listener<ComplexSamples>, IRealBufferProvider, IDecoderStateEventProvider, INoiseSquelchController
{
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);

View File

@@ -1,103 +0,0 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealSampleListener;
/**
* C4FM slicer to convert the output stream of the C4FMSymbolFilter into a
* stream of C4FM symbols.
*
* Supports registering listener(s) to receive normal and/or inverted symbol
* output streams.
*/
public class C4FMSlicer implements RealSampleListener
{
private static final float THRESHOLD = 2.0f;
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
/**
* Primary method for receiving output from the C4FMSymbolFilter. Slices
* (converts) the filtered sample value into a C4FMSymbol decision.
*/
@Override
public void receive( float sample )
{
if( sample > 0.0 )
{
if( sample >= THRESHOLD )
{
dispatch( Dibit.D01_PLUS_3 );
}
else
{
dispatch( Dibit.D00_PLUS_1 );
}
}
else
{
if( sample > -THRESHOLD )
{
dispatch( Dibit.D10_MINUS_1 );
}
else
{
dispatch( Dibit.D11_MINUS_3 );
}
}
}
/**
* Dispatches the symbol decision to any registered listeners
*/
private void dispatch( Dibit symbol )
{
mBroadcaster.receive( symbol );
}
/**
* Registers the listener to receive the normal (non-inverted) C4FM symbol
* stream.
*/
public void addListener( Listener<Dibit> listener )
{
mBroadcaster.addListener( listener );
}
/**
* Removes the listener
*/
public void removeListener( Listener<Dibit> listener )
{
mBroadcaster.removeListener( listener );
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,7 +33,7 @@ public class DecodeConfigP25Phase1 extends DecodeConfigP25
public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500;
public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000;
private P25P1Decoder.Modulation mModulation = P25P1Decoder.Modulation.C4FM;
private Modulation mModulation = Modulation.C4FM;
/**
* Constructs an instance
@@ -49,12 +49,12 @@ public class DecodeConfigP25Phase1 extends DecodeConfigP25
}
@JacksonXmlProperty(isAttribute = true, localName = "modulation")
public P25P1Decoder.Modulation getModulation()
public Modulation getModulation()
{
return mModulation;
}
public void setModulation(P25P1Decoder.Modulation modulation)
public void setModulation(Modulation modulation)
{
mModulation = modulation;
}

View File

@@ -1,44 +0,0 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
/**
* Listener interface to be notified each time a P25 sync pattern and data unit has been detected
* and the data unit is correct after error detection and correction and/or when the sync has been lost.
*/
public interface IP25P1DataUnitDetectListener
{
/**
* Indicates that a P25 sync has been detected and a P25 data unit was successfully decoded.
* @param dataUnitID that was contained in the detected NID
* @param nac or Network Access Code that was contained in the detected NID
* @param discardedDibits prior to detecting the P25 sync pattern
* @param bitErrors detected and corrected from both the sync pattern and the NID.
* @param correctedNid bits corrected by the BCH error correction code (temporary until message parsers are updated
*/
void dataUnitDetected(P25P1DataUnitID dataUnitID, int nac, int bitErrors, int discardedDibits, int[] correctedNid);
/**
* Indicates that sync has been lost on the dibit stream
*
* @param bitsProcessed since the last sync detect
*/
void syncLost(int bitsProcessed);
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,21 +14,36 @@
*
* 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.bits.BinaryMessage;
public interface C4FMFrameListener
/**
* P25 Phase 1 Modulations enumeration.
*/
public enum Modulation
{
/**
* Listener interface to receive the output of the C4FMMessageFramer.
*
* @param buffer - framed message without the sync pattern
* @param inverted - flag indicating if the message was received inverted
*/
public void receive( BinaryMessage buffer, boolean inverted );
CQPSK("Simulcast (LSM)"),
C4FM("Normal (C4FM)");
private final String mLabel;
/**
* Constructs an instance
* @param label pretty display
*/
Modulation(String label)
{
mLabel = label;
}
public String getLabel()
{
return mLabel;
}
public String toString()
{
return getLabel();
}
}

View File

@@ -0,0 +1,196 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Tracks the dominant NAC value for a channel to aid in NID error detection and verification.
*/
public class NACTracker
{
private static final Comparator<Tracker> COUNT_DESCENDING_COMPARATOR = new CountDescendingSorter();
private static final Comparator<Tracker> TIME_ASCENDING_COMPARATOR = new TimeAscendingSorter();
private final Map<Integer, Tracker> mTrackerMap = new HashMap<>();
private static final int MAX_TRACKER_COUNT = 3;
private static final int MIN_OBSERVATION_THRESHOLD = 3;
/**
* Removes all tracked NAC values. Invoke this method after an extended loss of sync.
*/
public void reset()
{
mTrackerMap.clear();
}
/**
* Tracks the NAC value. Each time the NAC value is observed, the observation count is incremented so that the
* dominant NAC value will have the highest observation count. Additionally, the last updated timestamp for the
* tracker is updated to now. When the tracker count exceeds the max tracker threshold, the tracker with the
* oldest updated timestamp is thrown out.
* @param nac to track
*/
public void track(int nac)
{
Tracker tracker = mTrackerMap.get(nac);
if(tracker == null)
{
mTrackerMap.put(nac, new Tracker(nac));
if(mTrackerMap.size() > MAX_TRACKER_COUNT)
{
List<Tracker> trackerList = new ArrayList<>(mTrackerMap.values());
trackerList.sort(TIME_ASCENDING_COMPARATOR);
Tracker oldest = trackerList.getFirst();
mTrackerMap.remove(oldest.nac());
}
}
else
{
tracker.increment();
}
}
/**
* Identifies the dominant NAC value by sorting the tracked NAC values in count order with the highest count NAC
* value identified as the dominant NAC. The dominant NAC must have at least 3 observations to be the dominant NAC.
* @return dominant tracked NAC value.
*/
public int getTrackedNAC()
{
if(mTrackerMap.isEmpty())
{
return 0;
}
List<Tracker> trackers = new ArrayList<>(mTrackerMap.values());
trackers.sort(COUNT_DESCENDING_COMPARATOR);
Tracker highestCount = trackers.getFirst();
if(highestCount.count() >= MIN_OBSERVATION_THRESHOLD)
{
return highestCount.nac();
}
return 0;
}
/**
* Tracked NAC value class.
*/
private class Tracker
{
private int mNAC;
private long mTimestamp = System.currentTimeMillis();
private int mCount = 1;
/**
* Constructs an instance to track the NAC. Note: count is set to 1 and timestamp is set to now.
* @param nac to track.
*/
private Tracker(int nac)
{
mNAC = nac;
}
/**
* Tracked NAC value for this tracker.
* @return nac value.
*/
private int nac()
{
return mNAC;
}
/**
* Increments the observation count for this tracker and updates the timestamp.
*/
public void increment()
{
mCount++;
//Don't let the count value rollover to negative
if(mCount < 0)
{
mCount = Integer.MAX_VALUE;
}
mTimestamp = System.currentTimeMillis();
}
/**
* Last observation timestamp.
* @return timestamp.
*/
public long timestamp()
{
return mTimestamp;
}
/**
* Observation count for this tracker.
* @return count
*/
public int count()
{
return mCount;
}
/**
* Pretty print.
*/
@Override
public String toString()
{
return "NAC [" + mNAC + "] COUNT [" + mCount + "] " + new Date(mTimestamp);
}
}
/**
* Sorts the tracked NACs in count order where index 0 is the highest tracked value.
*/
public static class CountDescendingSorter implements Comparator<Tracker>
{
@Override
public int compare(Tracker o1, Tracker o2)
{
//Simply swap the order for comparison to get the reverse count sort
return Integer.compare(o2.count(), o1.count());
}
}
/**
* Sorts the tracked NACs in time order where index 0 is the oldest tracked value.
*/
public static class TimeAscendingSorter implements Comparator<Tracker>
{
@Override
public int compare(Tracker o1, Tracker o2)
{
return Long.compare(o1.timestamp(), o2.timestamp());
}
}
}

View File

@@ -1,566 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.edac.BCH_63_16_11;
import io.github.dsheirer.sample.Listener;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1DataUnitDetector implements Listener<Dibit>, ISyncDetectListener
{
private final static Logger mLog = LoggerFactory.getLogger(P25P1DataUnitDetector.class);
private static final int DATA_UNIT_DIBIT_LENGTH = 57; //56 dibits plus 1 status symbol
private static final int SYNC_DIBIT_LENGTH = 24;
private static final int MAXIMUM_SYNC_MATCH_BIT_ERRORS = 9;
private P25P1SyncDetector mSyncDetector;
private NIDDelayBuffer mDataUnitBuffer = new NIDDelayBuffer();
private DibitDelayBuffer mSyncDelayBuffer = new DibitDelayBuffer(DATA_UNIT_DIBIT_LENGTH - SYNC_DIBIT_LENGTH);
private IP25P1DataUnitDetectListener mDataUnitDetectListener;
private boolean mInitialSyncTestProcessed = false;
private int mDibitsProcessed = 0;
private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
private P25P1DataUnitID mPreviousDataUnitId = P25P1DataUnitID.TERMINATOR_DATA_UNIT;
private int mNIDDetectionCount;
public P25P1DataUnitDetector(IP25P1DataUnitDetectListener dataUnitDetectListener, IPhaseLockedLoop phaseLockedLoop)
{
mDataUnitDetectListener = dataUnitDetectListener;
mSyncDetector = new P25P1SyncDetector(this, phaseLockedLoop);
}
/**
* Sets the sample rate for the phase inversion sync detector
*/
public void setSampleRate(double sampleRate)
{
mSyncDetector.setSampleRate(sampleRate);
}
public void reset()
{
mDibitsProcessed = 0;
mInitialSyncTestProcessed = false;
}
@Override
public void syncDetected(int bitErrors)
{
mInitialSyncTestProcessed = true;
checkForNid(bitErrors, false);
}
@Override
public void syncLost(int bitsProcessed)
{
dispatchSyncLoss(bitsProcessed);
}
private void dispatchSyncLoss(int bitsProcessed)
{
if(mDataUnitDetectListener != null)
{
mDataUnitDetectListener.syncLost(bitsProcessed);
}
}
@Override
public void receive(Dibit dibit)
{
mDibitsProcessed++;
//Broadcast a sync loss every 4800 dibits/9600 bits ... or 1x per second for phase 1
if(mDibitsProcessed > 4864)
{
dispatchSyncLoss(9600);
mDibitsProcessed -= 4800;
}
mDataUnitBuffer.put(dibit);
//Feed the sync detect with a 32 dibit delay from the data unit buffer so that if/when
//a sync detect occurs, the data unit buffer is already filled with both the sync dibits
//and the NID dibits and we can test for a valid NID
mSyncDetector.receive(mSyncDelayBuffer.getAndPut(dibit));
//If the sync detector doesn't fire and we've processed enough dibits for a sync/nid sequence
//immediately following a valid message, then test for a NID anyway ... maybe the sync was corrupted
if(!mInitialSyncTestProcessed && mDibitsProcessed == DATA_UNIT_DIBIT_LENGTH)
{
mInitialSyncTestProcessed = true;
checkForNid(mSyncDetector.getPrimarySyncMatchErrorCount(), true);
}
}
/**
* Chects/tests the contents of the data unit buffer for a valid NID when a sync pattern is detected
* or when commanded following a valid message sequence
*
* @param bitErrorCount when comparing the sync pattern to the received bit sequence
* @param forcedCheck indicates if the NID check was forced, meaning that the primary sync detector
* did not detect a sync, however we expect there to be a message sync following a successfully
* decoded and framed preceeding messsage. In these instances, the framer will allow for a less
* than perfect match on the sync pattern, as long as the NID passes error check.
*/
private void checkForNid(int bitErrorCount, boolean forcedCheck)
{
if(bitErrorCount <= MAXIMUM_SYNC_MATCH_BIT_ERRORS)
{
int[] nid = mSyncDelayBuffer.getNID();
int[] correctedNid = new int[63];
//If decoder indicates there are no unrecoverable errors ....
if(!mNIDDecoder.decode(nid, correctedNid))
{
mNIDDetectionCount++;
int nidBitErrorCount = getBitErrorCount(nid, correctedNid);
if(mDataUnitDetectListener != null)
{
mPreviousDataUnitId = getDataUnitID(correctedNid);
mDataUnitDetectListener.dataUnitDetected(mPreviousDataUnitId, getNAC(correctedNid),
(bitErrorCount + nidBitErrorCount), (mDibitsProcessed - DATA_UNIT_DIBIT_LENGTH), correctedNid);
}
}
else if(mPreviousDataUnitId == P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1)
{
//We have a good sync match, but the NID didn't pass error control and we're in the middle
//of voice call, so treat this message as voice message, but set the previous duid to
//terminator so we can end if there isn't a subsequent voice message
mDataUnitDetectListener.dataUnitDetected(P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_2, -1,
(bitErrorCount + 64), (mDibitsProcessed - DATA_UNIT_DIBIT_LENGTH), new int[63]);
mPreviousDataUnitId = P25P1DataUnitID.TERMINATOR_DATA_UNIT;
}
else if(mPreviousDataUnitId == P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_2)
{
//We have a good sync match, but the NID didn't pass error control and we're in the middle
//of voice call, so treat this message as voice message, but set the previous duid to
//terminator so we can end if there isn't a subsequent voice message
mDataUnitDetectListener.dataUnitDetected(P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1, -1,
(bitErrorCount + 64), (mDibitsProcessed - DATA_UNIT_DIBIT_LENGTH), new int[63]);
mPreviousDataUnitId = P25P1DataUnitID.TERMINATOR_DATA_UNIT;
}
}
}
/**
* Determines the data unit ID present in the nid value.
* @param nid in reverse bit order
* @return
*/
public P25P1DataUnitID getDataUnitID(int[] nid)
{
int duid = 0;
if(nid[47] == 1)
{
duid ^= 1;
}
if(nid[48] == 1)
{
duid ^= 2;
}
if(nid[49] == 1)
{
duid ^= 4;
}
if(nid[50] == 1)
{
duid ^= 8;
}
return P25P1DataUnitID.fromValue(duid);
}
/**
* Determines the Network Access Code (NAC) present in the nid value.
* @param nid in reverse bit order
* @return nac
*/
public int getNAC(int[] nid)
{
int nac = 0;
if(nid[51] == 1)
{
nac ^= 1;
}
if(nid[52] == 1)
{
nac ^= 2;
}
if(nid[53] == 1)
{
nac ^= 4;
}
if(nid[54] == 1)
{
nac ^= 8;
}
if(nid[55] == 1)
{
nac ^= 16;
}
if(nid[56] == 1)
{
nac ^= 32;
}
if(nid[57] == 1)
{
nac ^= 64;
}
if(nid[58] == 1)
{
nac ^= 128;
}
if(nid[59] == 1)
{
nac ^= 256;
}
if(nid[60] == 1)
{
nac ^= 512;
}
if(nid[61] == 1)
{
nac ^= 1024;
}
if(nid[62] == 1)
{
nac ^= 2048;
}
return nac;
}
public int getNIDDetectionCount()
{
return mNIDDetectionCount;
}
public class NIDDelayBuffer extends DibitDelayBuffer
{
public NIDDelayBuffer()
{
super(DATA_UNIT_DIBIT_LENGTH);
}
public int[] getNID()
{
int[] nid = new int[64];
int nidPointer = 0;
int bufferPointer = mPointer;
while(nidPointer < 64)
{
if(mBuffer[bufferPointer].getBit2())
{
nid[nidPointer] = 1;
}
nidPointer++;
if(mBuffer[bufferPointer].getBit1())
{
nid[nidPointer] = 1;
}
nidPointer++;
bufferPointer--;
if(bufferPointer < 0)
{
bufferPointer += mBuffer.length;
}
//Check for the status symbol
if(nidPointer == 70)
{
nidPointer += 2;
}
}
return nid;
}
}
/**
* Circular buffer for storing and accessing dibits.
*/
public class DibitDelayBuffer
{
protected Dibit[] mBuffer;
protected int mPointer;
/**
* Constructs a dibit delay buffer of the specified length
*/
public DibitDelayBuffer(int length)
{
mBuffer = new Dibit[length];
//Preload the buffer to avoid null pointers
for(int x = 0; x < length; x++)
{
mBuffer[x] = Dibit.D00_PLUS_1;
}
}
/**
* Fetches the NID in reverse order format required for the ECC code
*/
public int[] getNID()
{
int[] nid = new int[63];
int nidPointer = 0;
int bufferPointer = mPointer - 1;
if(bufferPointer < 0)
{
bufferPointer += mBuffer.length;
}
//Skip bit 2 of the starting (ie last) dibit ... we only want 63 of the 64 bits from the dibit buffer
if(mBuffer[bufferPointer].getBit1())
{
nid[nidPointer] = 1;
}
nidPointer++;
bufferPointer--;
if(bufferPointer < 0)
{
bufferPointer += mBuffer.length;
}
while(nidPointer < 63)
{
if(mBuffer[bufferPointer].getBit2())
{
nid[nidPointer] = 1;
}
nidPointer++;
if(mBuffer[bufferPointer].getBit1())
{
nid[nidPointer] = 1;
}
nidPointer++;
bufferPointer--;
//Check for and skip the status symbol that gets inserted after 70 bits, meaning
//it will be located in the buffer after 22 NID bits
if(nidPointer == 41)
{
bufferPointer--;
}
if(bufferPointer < 0)
{
bufferPointer += mBuffer.length;
}
}
return nid;
}
public void log()
{
StringBuilder sb = new StringBuilder();
int counter = 0;
int pointer = mPointer;
while(counter < mBuffer.length)
{
sb.append(mBuffer[pointer].getBit1() ? "1" : "0");
sb.append(mBuffer[pointer++].getBit2() ? "1" : "0");
if(pointer >= mBuffer.length)
{
pointer = 0;
}
counter++;
}
if(this instanceof NIDDelayBuffer)
{
mLog.debug("NIDBUF: " + sb + " Length:" + mBuffer.length);
}
else
{
mLog.debug("SYNBUF: " + sb + " Length:" + mBuffer.length);
}
}
/**
* Returns an ordered buffer of the internal circular buffer contents.
*/
public Dibit[] getBuffer()
{
Dibit[] transferBuffer = new Dibit[mBuffer.length];
int transferBufferPointer = 0;
int bufferPointer = mPointer;
while(transferBufferPointer < transferBuffer.length)
{
transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
if(bufferPointer >= mBuffer.length)
{
bufferPointer = 0;
}
}
return transferBuffer;
}
public int[] getBufferAsArray()
{
Dibit[] dibits = getBuffer();
int[] bits = new int[dibits.length * 2];
for(int x = 0; x < dibits.length; x++)
{
if(dibits[x].getBit1())
{
bits[x * 2] = 1;
}
if(dibits[x].getBit2())
{
bits[x * 2 + 1] = 1;
}
}
return bits;
}
/**
* Places the dibit into the internal circular buffer, overwriting the oldest dibit.
*/
public void put(Dibit dibit)
{
//Note: this check is necessary where the previous runnable was interrupted and left the buffer
//pointer pointing past the end of the array length.
if(mPointer >= mBuffer.length)
{
mPointer = 0;
}
mBuffer[mPointer++] = dibit;
if(mPointer >= mBuffer.length)
{
mPointer = 0;
}
}
/**
* Places the dibit into the internal circular buffer, overwriting and returning the
* oldest dibit.
*/
public Dibit getAndPut(Dibit dibit)
{
Dibit toReturn = mBuffer[mPointer];
put(dibit);
return toReturn;
}
}
public static int[] reverse(int[] values)
{
int[] reversed = new int[values.length];
for(int x = 0; x < values.length; x++)
{
reversed[values.length - x - 1] = values[x];
}
return reversed;
}
public static void logNID(int[] nid, boolean corrected)
{
StringBuilder sb = new StringBuilder();
for(int value: nid)
{
sb.append(value);
}
if(corrected)
{
mLog.debug("C* NID: " + sb);
}
else
{
mLog.debug(" NID: " + sb);
}
}
public static int getBitErrorCount(int[] a, int[] b)
{
Validate.isTrue(a.length == b.length, "Array lengths must be the same");
int count = 0;
for(int x = 0; x < a.length; x++)
{
if(a[x] != b[x])
{
count++;
}
}
return count;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,52 +18,85 @@
*/
package io.github.dsheirer.module.decode.p25.phase1;
import java.util.EnumSet;
/**
* The Data Unit ID (DUID) is part of the Network ID (NID) field and indicates the type of message.
*/
public enum P25P1DataUnitID
{
HEADER_DATA_UNIT(0, 648 + 10, true, "HDU "),
UNKNOWN_1(1, -1, false, "UNK01"),
UNKNOWN_2(2, -1, false, "UNK02"),
TERMINATOR_DATA_UNIT(3, 28, true, "TDU "),
UNKNOWN_4(4, -1, false, "UNK04"),
LOGICAL_LINK_DATA_UNIT_1(5, 1568, true, "LDU1 "),
VSELP1(6, 64 + 1616, false, "VSEL1"),
UNKNOWN_8(8, -1, false, "UNK08"),
VSELP2(9, 1616 + 64, false, "VSEL2"),
LOGICAL_LINK_DATA_UNIT_2(10, 1568, true, "LDU2 "),
UNKNOWN_11(11, -1, false, "UNK11"),
PACKET_HEADER_DATA_UNIT(12, 196, false, "PDUH "),
PACKET_DATA_UNIT(-1, 196, false, "PDU "),
UNKNOWN_13(13, -1, false, "UNK13"),
UNKNOWN_14(14, -1, false, "UNK14"),
TERMINATOR_DATA_UNIT_LINK_CONTROL(15, 308, true, "TDULC"),
HEADER_DATA_UNIT(0, 648, 11, 10, "HDU "),
TERMINATOR_DATA_UNIT(3, 0, 2, 28,"TDU "),
LOGICAL_LINK_DATA_UNIT_1(5, 1568, 24, 0, "LDU1 "),
TRUNKING_SIGNALING_BLOCK_1(7, 196, 5, 42, "TSBK1"),
LOGICAL_LINK_DATA_UNIT_2(10, 1568, 24, 0, "LDU2 "),
PACKET_DATA_UNIT(12, 196, 5, 42, "PDU"),
TERMINATOR_DATA_UNIT_LINK_CONTROL(15, 288, 6, 20, "TDULC"),
//Set to 2x dibits longer than longest (ie LDU) message type so we can force a detect if it's followed by a sync.
PLACE_HOLDER(-1, 1572, 24, 0, "PLACEHOLDER"), //Originally: 1686
//The following are not true data unit identifiers, rather they are used as identifiers
ALTERNATE_MULTI_BLOCK_TRUNKING_CONTROL(-1, -1, false, "AMBTC"),
IP_PACKET_DATA(-1, 0, false, "IPPKT"),
SUBNETWORK_DEPENDENT_CONVERGENCE_PROTOCOL(-1, 0, false, "SNDCP"),
TRUNKING_SIGNALING_BLOCK_1(7, 196, false, "TSBK1"),
TRUNKING_SIGNALING_BLOCK_2(7, 196, false, "TSBK2"),
TRUNKING_SIGNALING_BLOCK_3(7, 196, false, "TSBK3"),
UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL(-1, -1, false, "UMBTC"),
ALTERNATE_MULTI_BLOCK_TRUNKING_CONTROL(-1, -1, 0, 0, "AMBTC"),
IP_PACKET_DATA(-1, 0, 0, 0, "IPPKT"),
PACKET_DATA_UNIT_BLOCK_1(-1, 196 * 2, 8, 56, "PDU"),
PACKET_DATA_UNIT_BLOCK_2(-1, 196 * 3, 10, 0, "PDU"),
PACKET_DATA_UNIT_BLOCK_3(-1, 196 * 4, 13, 14, "PDU"),
PACKET_DATA_UNIT_BLOCK_4(-1, 196 * 5, 16, 28, "PDU"),
PACKET_DATA_UNIT_BLOCK_5(-1, 196 * 6, 19, 42, "PDU"),
SUBNETWORK_DEPENDENT_CONVERGENCE_PROTOCOL(-1, 0, 0, 0, "SNDCP"),
TRUNKING_SIGNALING_BLOCK_2(7, 196 * 2, 8, 56, "TSBK2"),
TRUNKING_SIGNALING_BLOCK_3(7, 196 * 3, 10, 0,"TSBK3"),
UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL(-1, -1, 0, 0, "UMBTC"),
UNKNOWN(-1, -1, false, "UNKN ");
UNKNOWN(-1, -1, 0, 0, "UNKNOWN");
private int mValue;
private int mMessageLength;
private boolean mTrailingStatusDibit;
private String mLabel;
private final int mValue;
private final int mMessageLength;
private final int mElapsedDibitLength;
private final String mLabel;
P25P1DataUnitID(int value, int length, boolean trailingStatusDibit, String label)
/**
* Constructs a Data Unit ID (DUID) entry
* @param value transmitted in the NID
* @param messageLength in bits
* @param statusDibits count
* @param nullBits trailing null bits used to pad the message length to the final status dibit
* @param label for the DUID
*/
P25P1DataUnitID(int value, int messageLength, int statusDibits, int nullBits, String label)
{
mValue = value;
mMessageLength = length;
mTrailingStatusDibit = trailingStatusDibit;
mMessageLength = messageLength + nullBits;
//Elapsed dibit length starts with: sync(24) + nid(32) = 56 dibits
mElapsedDibitLength = 56 + (messageLength / 2) + statusDibits + (nullBits / 2);
mLabel = label;
}
public static final EnumSet<P25P1DataUnitID> VALID_PRIMARY_DUIDS = EnumSet.of(HEADER_DATA_UNIT, TERMINATOR_DATA_UNIT,
LOGICAL_LINK_DATA_UNIT_1, LOGICAL_LINK_DATA_UNIT_2, PACKET_DATA_UNIT, PACKET_DATA_UNIT_BLOCK_1,
TRUNKING_SIGNALING_BLOCK_1, TERMINATOR_DATA_UNIT_LINK_CONTROL);
public static final EnumSet<P25P1DataUnitID> TSBK_DATA_UNITS = EnumSet.of(TRUNKING_SIGNALING_BLOCK_1,
TRUNKING_SIGNALING_BLOCK_2, TRUNKING_SIGNALING_BLOCK_3);
/**
* Indicates if this DUID is a primary DUID carried by the NID.
* @return true if a primary DUID.
*/
public boolean isValidPrimaryDUID()
{
return VALID_PRIMARY_DUIDS.contains(this);
}
/**
* Indicates if this DUID is a TSBK 1, 2, or 3
*/
public boolean isTSBK()
{
return TSBK_DATA_UNITS.contains(this);
}
/**
* Data Unit ID value
*/
@@ -80,6 +113,15 @@ public enum P25P1DataUnitID
return mMessageLength;
}
/**
* Elapsed dibit length from sync detection to next sync detection for this DUID
* @return elapsed dibit length.
*/
public int getElapsedDibitLength()
{
return mElapsedDibitLength;
}
/**
* Short display label
*/
@@ -93,7 +135,7 @@ public enum P25P1DataUnitID
*/
public boolean hasTrailingStatusDibit()
{
return mTrailingStatusDibit;
return false;
}
/**
@@ -105,38 +147,20 @@ public enum P25P1DataUnitID
{
case 0:
return HEADER_DATA_UNIT;
case 1:
return UNKNOWN_1;
case 2:
return UNKNOWN_2;
case 3:
return TERMINATOR_DATA_UNIT;
case 4:
return UNKNOWN_4;
case 5:
return LOGICAL_LINK_DATA_UNIT_1;
case 6:
return VSELP1;
case 7:
return TRUNKING_SIGNALING_BLOCK_1;
case 8:
return UNKNOWN_8;
case 9:
return VSELP2;
case 10:
return LOGICAL_LINK_DATA_UNIT_2;
case 11:
return UNKNOWN_11;
case 12:
return PACKET_HEADER_DATA_UNIT;
case 13:
return UNKNOWN_13;
case 14:
return UNKNOWN_14;
return PACKET_DATA_UNIT;
case 15:
return TERMINATOR_DATA_UNIT_LINK_CONTROL;
default:
throw new IllegalArgumentException("Data Unit ID must be in range 0 - 15");
return UNKNOWN;
}
}
}

View File

@@ -1,227 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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 com.google.common.eventbus.Subscribe;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
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.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import java.nio.ByteBuffer;
public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
IComplexSamplesListener, Listener<ComplexSamples>, IByteBufferProvider
{
private double mSampleRate;
private Broadcaster<Dibit> mDibitBroadcaster = new Broadcaster<>();
private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300);
private P25P1MessageProcessor mMessageProcessor;
private Listener<SourceEvent> mSourceEventListener;
private double mSymbolRate;
protected PowerMonitor mPowerMonitor = new PowerMonitor();
public P25P1Decoder(double symbolRate)
{
mSymbolRate = symbolRate;
mMessageProcessor = new P25P1MessageProcessor();
mMessageProcessor.setMessageListener(getMessageListener());
getDibitBroadcaster().addListener(mByteBufferAssembler);
}
/**
* 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);
}
@Override
public void setSourceEventListener(Listener<SourceEvent> listener )
{
super.setSourceEventListener(listener);
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
super.removeSourceEventListener();
mPowerMonitor.setSourceEventListener(null);
}
/**
* 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 >9600 (2 * " +
getSymbolRate() + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
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 new Listener<SourceEvent>()
{
@Override
public void receive(SourceEvent sourceEvent)
{
process(sourceEvent);
}
};
}
/**
* Sub-class processing of received source events
* @param sourceEvent
*/
protected abstract void process(SourceEvent sourceEvent);
/**
* Listener interface to receive reusable complex buffers
*/
@Override
public Listener<ComplexSamples> getComplexSamplesListener()
{
return P25P1Decoder.this;
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE1;
}
protected P25P1MessageProcessor getMessageProcessor()
{
return mMessageProcessor;
}
public abstract Modulation getModulation();
public enum Modulation
{
CQPSK("Simulcast (LSM)", "LSM"),
C4FM("Normal (C4FM)", "C4FM");
private String mLabel;
private String mShortLabel;
private Modulation(String label, String shortLabel)
{
mLabel = label;
mShortLabel = shortLabel;
}
public String getLabel()
{
return mLabel;
}
public String getShortLabel()
{
return mShortLabel;
}
public String toString()
{
return getLabel();
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,93 +16,138 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
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.DQPSKDecisionDirectedDemodulator;
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.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
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.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1DecoderC4FM extends P25P1Decoder
/**
* APCO25 Phase 1 decoder. Decimates incoming sample buffers to as close as possible to 25 kHz for ~5 samples per
* symbol. Employs baseband and pulse shaping filters. Demodulates the complex baseband (I/Q) sample stream using
* SIMD differential demodulation of the entire sample stream. The C4FM demodulator (mSymbolProcessor) performs sync
* detection, timing correction and PLL signal mistune correction. Message framer performs message framing and message
* creation. A registered message listener receives the detected and framed 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 P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
ISourceEventListener, ISourceEventProvider, Listener<ComplexSamples>
{
private final static Logger mLog = LoggerFactory.getLogger(P25P1DecoderC4FM.class);
private static final Logger LOGGER = LoggerFactory.getLogger(P25P1DecoderC4FM.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
private static final int SYMBOL_RATE = 4800;
private static final Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
protected P25P1MessageFramer mMessageFramer;
protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl();
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
protected IRealFilter mIBasebandFilter;
protected IRealFilter mQBasebandFilter;
private final P25P1DemodulatorC4FM mSymbolProcessor;
private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer();
private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor();
private final PowerMonitor mPowerMonitor = new PowerMonitor();
private DifferentialDemodulatorFloat mDemodulator;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
private IRealFilter mPulseShapingFilterI;
private IRealFilter mPulseShapingFilterQ;
@Override
public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE1;
}
/**
* P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a
* decision-directed phase and timing error detector.
*/
public P25P1DecoderC4FM()
{
super(4800.0);
setSampleRate(25000.0);
mMessageProcessor.setMessageListener(getMessageListener());
mSymbolProcessor = new P25P1DemodulatorC4FM(mMessageFramer, this);
}
@Override
public void start()
public String getProtocolDescription()
{
super.start();
mQPSKDemodulator.start();
}
@Override
public void stop()
{
super.stop();
mQPSKDemodulator.stop();
return "P25 Phase 1 C4FM";
}
/**
* Sets the sample rate and configures internal decoder components.
* @param sampleRate of the channel to decode
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300);
mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
if(mMessageFramer != null)
if(sampleRate <= SYMBOL_RATE * 2)
{
getDibitBroadcaster().removeListener(mMessageFramer);
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (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 P25P1MessageFramer(mCostasLoop, DecoderType.P25_PHASE1.getProtocol().getBitRate());
mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
mMessageFramer.setListener(getMessageProcessor());
mMessageFramer.setSampleRate(sampleRate);
mPowerMonitor.setSampleRate((int)sampleRate);
mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
int decimation = 1;
//Identify decimation that gets us as close to 4.0 Samples Per Symbol as possible (19.2 kHz)
while((sampleRate / decimation) >= 38400)
{
decimation *= 2;
}
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
float decimatedSampleRate = (float)sampleRate / decimation;
int symbolLength = 16;
float rrcAlpha = 0.2f;
float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / SYMBOL_RATE,
symbolLength, rrcAlpha);
mPulseShapingFilterI = new RealFIRFilter(taps);
mPulseShapingFilterQ = new RealFIRFilter(taps);
mBasebandFilterI = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mBasebandFilterQ = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mDemodulator = DifferentialDemodulatorFactory.getFloatDemodulator(decimatedSampleRate, SYMBOL_RATE);
mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol());
mMessageFramer.setListener(mMessageProcessor);
mMessageProcessor.setMessageListener(getMessageListener());
}
/**
@@ -112,91 +157,292 @@ public class P25P1DecoderC4FM extends P25P1Decoder
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
float[] i = mDecimationFilterI.decimateReal(samples.i());
float[] q = mDecimationFilterQ.decimateReal(samples.q());
//Process the buffer for power meter measurements (before gain is applied)
//Process buffer for channel power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
i = mPulseShapingFilterI.filter(i);
q = mPulseShapingFilterQ.filter(q);
// PI/4 DQPSK differential demodulation
float[] demodulated = mDemodulator.demodulate(i, q);
//Process demodulated samples into symbols and apply message sync detection and framing.
mSymbolProcessor.process(demodulated);
}
/**
* Constructs a baseband filter for this decoder using the current sample rate
*/
private float[] getBasebandFilter()
private float[] getBasebandFilter(double sampleRate)
{
//Attempt to reuse a cached (ie already-designed) filter if available
float[] filter = mBasebandFilters.get(getSampleRate());
if(filter == null)
if(BASEBAND_FILTERS.containsKey(sampleRate))
{
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate((int)getSampleRate())
.passBandCutoff(5100)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandAmplitude(0.0)
.stopBandStart(6500)
.stopBandRipple(0.01)
.build();
try
{
filter = FilterFactory.getTaps(specification);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
}
if(filter != null)
{
mBasebandFilters.put(getSampleRate(), filter);
}
else
{
throw new IllegalStateException("Couldn't design a C4FM baseband filter for sample rate: " + getSampleRate());
}
return BASEBAND_FILTERS.get(sampleRate);
}
return filter;
FIRFilterSpecification specification = FIRFilterSpecification
.lowPassBuilder()
.sampleRate(sampleRate)
.passBandCutoff(5200)
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
.stopBandAmplitude(0.0).stopBandStart(6500) //6500
.stopBandRipple(0.01).build();
float[] coefficients = null;
try
{
coefficients = FilterFactory.getTaps(specification);
BASEBAND_FILTERS.put(sampleRate, coefficients);
}
catch(Exception fde) //FilterDesignException
{
System.out.println("Error");
}
if(coefficients == null)
{
throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]");
}
return coefficients;
}
/**
* 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)
{
mSymbolProcessor.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;
}
/**
* Sets the source event listener to receive source events from this decoder.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
super.setSourceEventListener(listener);
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
super.removeSourceEventListener();
mPowerMonitor.setSourceEventListener(null);
}
@Override
public void start()
{
super.start();
mMessageFramer.start();
}
@Override
public void stop()
{
super.stop();
mMessageFramer.stop();
}
/**
* Process source events
*/
private void process(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mCostasLoop.reset();
setSampleRate(sourceEvent.getValue().doubleValue());
break;
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
mCostasLoop.reset();
mSymbolProcessor.resetPLL();
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
}
}
/**
* P25 modulation supported by this decoder
*/
public Modulation getModulation()
@Override
public Listener<ComplexSamples> getComplexSamplesListener()
{
return Modulation.C4FM;
return this;
}
/**
* Resets this decoder to prepare for processing a new channel
*/
@Override
public void reset()
public static void main(String[] args)
{
mCostasLoop.reset();
mFrequencyCorrectionSyncMonitor.reset();
LOGGER.info("Starting ...");
// String directory = "D:\\DQPSK Equalizer Research - P25\\"; //Windows
String directory = "/media/denny/T9/DQPSK Equalizer Research - P25/"; //Linux
String file = directory + "P25-S1-Conventional-repeater-20241115_212221_469325000_QPS_Digital_Kynoch_Kynoch_Digital_59_baseband.wav";
// String file = directory + "P25-S3-C4FM-20241225_040459_152517500_NYSEG_Onondaga_Control_30_baseband.wav";
boolean autoReplay = false;
P25P1DecoderC4FM decoder = new P25P1DecoderC4FM();
decoder.start();
UserPreferences userPreferences = new UserPreferences();
P25P1AudioModule audio1 = new P25P1AudioModule(userPreferences, new AliasList(""));
decoder.setMessageListener(new Listener<>()
{
private long mBitCounter = 1;
private int mBitErrorCounter;
private int mValidMessageCounter;
private int mTotalMessageCounter;
@Override
public void receive(IMessage iMessage)
{
int errors = 0;
audio1.receive(iMessage);
if(iMessage instanceof P25P1Message message)
{
mBitCounter += 288;
if(message.getMessage() != null)
{
errors = message.getMessage().getCorrectedBitCount();
}
mTotalMessageCounter++;
if(mTotalMessageCounter == 492)
{
int a = 0;
}
if(message.isValid())
{
mBitErrorCounter += Math.max(errors, 0);
mValidMessageCounter++;
}
}
else if(iMessage instanceof LCMessage lcw)
{
mTotalMessageCounter++;
errors = lcw.getMessage().getCorrectedBitCount();
if(lcw.isValid())
{
mBitErrorCounter += errors;
mValidMessageCounter++;
}
}
double bitErrorRate = (double)mBitErrorCounter / (double)mBitCounter * 100.0;
boolean logEverything = true;
boolean logFLC = true;
boolean logSLC = true;
boolean logCACH = true;
boolean logIdles = true;
if(logEverything)
{
if(iMessage.toString().contains("PLACEHOLDER"))
{
int a = 0;
}
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
else
{
if (logFLC)
{
if(iMessage instanceof FullLCMessage)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
else if(iMessage instanceof Terminator terminator)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
else if(iMessage instanceof DataMessageWithLinkControl)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
}
if(logCACH && iMessage instanceof DMRBurst burst && burst.hasCACH())
{
System.out.println("CACH:" + burst.getCACH());
}
if(logSLC && iMessage instanceof ShortLCMessage)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
if(logIdles && iMessage instanceof SyncLossMessage)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
}
}
});
try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay))
{
source.setListener(iNativeBuffer -> {
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
while(it.hasNext())
{
decoder.receive(it.next());
}
});
source.start();
decoder.setSampleRate(source.getSampleRate());
while(true)
{
source.next(2048, true);
}
}
catch(IOException ioe)
{
LOGGER.error("Error", ioe);
}
LOGGER.info("Finished");
}
}

View File

@@ -1,126 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.psk.DQPSKDecisionDirectedDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
public class P25P1DecoderC4FMInstrumented extends P25P1DecoderC4FM
{
private Listener<Double> mPLLPhaseErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mComplexSymbolListener;
private Listener<ComplexSamples> mFilteredSymbolListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
/**
* Instrumented version of the P25 C4FM decoder that supports registering listeners to provide access to data as
* it is being processed by the decoder.
*/
public P25P1DecoderC4FMInstrumented()
{
}
/**
* Primary method for processing incoming complex sample buffers
* @param samples containing channelized complex samples
*/
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}
//Process the buffer for power meter measurements (before gain is applied)
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}
/**
* Overrides this method so we can correctly configure for instrumented operations
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer =
new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
mQPSKDemodulator = instrumented;
instrumented.setComplexSymbolListener(mComplexSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
instrumented.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
}
public void setComplexSymbolListener(Listener<Complex> listener)
{
mComplexSymbolListener = listener;
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
}
public void setFilteredBufferListener(Listener<ComplexSamples> listener)
{
mFilteredSymbolListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,171 +16,402 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.alias.AliasList;
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.FIRFilterSpecification;
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.dsp.window.WindowType;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
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.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1DecoderLSM extends P25P1Decoder
/**
* APCO25 Phase 1 Linear Simulcast Modulation (LSM) decoder. Decimates incoming sample buffers to as close as possible
* to 25 kHz for ~5 samples per symbol. Employs baseband and pulse shaping filters. Incorporates an demodulator to
* process complex baseband (I/Q) sample stream into soft symbols with soft sync detection and message framing. A
* registered message listener receives the detected and framed 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 P25P1DecoderLSM extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
ISourceEventProvider, Listener<ComplexSamples>
{
private final static Logger mLog = LoggerFactory.getLogger(P25P1DecoderLSM.class);
private static final Logger LOGGER = LoggerFactory.getLogger(P25P1DecoderLSM.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
private static final Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
private static final int SYMBOL_RATE = 4800;
protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
private final P25P1DemodulatorLSM mDemodulator;
private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer();
private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor();
private final PowerMonitor mPowerMonitor = new PowerMonitor();
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
private IRealFilter mPulseShapingFilterI;
private IRealFilter mPulseShapingFilterQ;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
protected IRealFilter mIBasebandFilter;
protected IRealFilter mQBasebandFilter;
protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl();
protected DQPSKGardnerDemodulator mQPSKDemodulator;
protected P25P1MessageFramer mMessageFramer;
protected CostasLoop mCostasLoop;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
@Override
public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE1;
}
/**
* P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and
* a gardner timing error detector.
*/
public P25P1DecoderLSM()
{
super(4800.0);
setSampleRate(25000.0);
mMessageProcessor.setMessageListener(getMessageListener());
mDemodulator = new P25P1DemodulatorLSM(mMessageFramer, this);
}
@Override
public String getProtocolDescription()
{
return "P25 Phase 1 LSM";
}
/**
* Sets the sample rate and configures internal components.
* @param sampleRate of the channel to decode
*/
public void setSampleRate(double sampleRate)
{
if(sampleRate <= SYMBOL_RATE * 2)
{
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
SYMBOL_RATE + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
int decimation = 1;
//Identify decimation that gets as close to 4.0 Samples Per Symbol as possible (4800 x 4.0 = 19.2 kHz)
while((sampleRate / decimation) >= 38400)
// while((sampleRate / decimation) > 50000)
{
decimation *= 2;
}
try
{
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
}
catch(Exception e)
{
LOGGER.error("Error getting decimation filter for sample rate [" + sampleRate + "] decimation [" + decimation + "]");
}
float decimatedSampleRate = (float)sampleRate / decimation;
int symbolLength = 16;
float rolloff = 0.2f;
float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / 4800.0, symbolLength, rolloff);
mPulseShapingFilterI = new RealFIRFilter(taps);
mPulseShapingFilterQ = new RealFIRFilter(taps);
mBasebandFilterI = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mBasebandFilterQ = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mDemodulator.setSamplesPerSymbol(decimatedSampleRate / (float)SYMBOL_RATE);
mMessageFramer.setListener(mMessageProcessor);
}
/**
* Primary method for processing incoming complex sample buffers
* @param samples containing channelized complex samples
*/
@Override
public void receive(ComplexSamples samples)
{
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = samples.i();
float[] q = samples.q();
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
//Process buffer for power measurements
mPowerMonitor.process(i, q);
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
i = mPulseShapingFilterI.filter(i);
q = mPulseShapingFilterQ.filter(q);
//Demodulate samples into symbols with timing, sync detection, and message framing.
mDemodulator.process(i, q);
}
/**
* Constructs a baseband filter for this decoder using the current sample rate
*/
private float[] getBasebandFilter(double sampleRate)
{
if(BASEBAND_FILTERS.containsKey(sampleRate))
{
return BASEBAND_FILTERS.get(sampleRate);
}
FIRFilterSpecification specification = FIRFilterSpecification
.lowPassBuilder()
.sampleRate(sampleRate)
.passBandCutoff(7250)
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
.stopBandAmplitude(0.0).stopBandStart(8000) //6500
.stopBandRipple(0.01).build();
float[] coefficients = null;
try
{
coefficients = FilterFactory.getTaps(specification);
BASEBAND_FILTERS.put(sampleRate, coefficients);
}
catch(Exception fde) //FilterDesignException
{
System.out.println("Error");
}
if(coefficients == null)
{
throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]");
}
return coefficients;
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void setBufferListener(Listener<ByteBuffer> listener)
{
mDemodulator.setBufferListener(listener);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void removeBufferListener(Listener<ByteBuffer> listener)
{
mDemodulator.setBufferListener(null);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public boolean hasBufferListeners()
{
return mDemodulator.hasBufferListener();
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return this::process;
}
/**
* Sets the source event listener to receive source events from this decoder.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
super.setSourceEventListener(listener);
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
mPowerMonitor.setSourceEventListener(null);
}
@Override
public void start()
{
super.start();
mQPSKDemodulator.start();
mMessageFramer.start();
}
@Override
public void stop()
{
super.stop();
mQPSKDemodulator.stop();
mMessageFramer.stop();
}
/**
* Sets or changes the channel sample rate
*
* @param sampleRate in hertz
* Process source events
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_200);
mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
//The Costas Loop receives symbol-inversion correction requests when detected.
//The PLL gain monitor receives sync detect/loss signals from the message framer
if(mMessageFramer != null)
{
getDibitBroadcaster().removeListener(mMessageFramer);
}
mMessageFramer = new P25P1MessageFramer(mCostasLoop, DecoderType.P25_PHASE1.getProtocol().getBitRate());
mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
mMessageFramer.setListener(getMessageProcessor());
mMessageFramer.setSampleRate(sampleRate);
mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
}
/**
* Primary method for receiving incoming channel samples
*/
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(samples.timestamp());
//The filter will decrement the user count when finished
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}
/**
* Processes source events - updates buffer and decoder when sample rate is established/changed and resets the
* PLL after frequency changes/corrections
*/
@Override
protected void process(SourceEvent sourceEvent)
private void process(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
mDemodulator.resetPLL();
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
mCostasLoop.reset();
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
mCostasLoop.reset();
break;
}
}
/**
* Resets this decoder to prepare for processing a new channel
*/
@Override
public void reset()
public Listener<ComplexSamples> getComplexSamplesListener()
{
mCostasLoop.reset();
mFrequencyCorrectionSyncMonitor.reset();
return this;
}
public Modulation getModulation()
public static void main(String[] args)
{
return Modulation.CQPSK;
}
LOGGER.info("Starting ...");
/**
* Constructs a baseband filter for this decoder using the current sample rate
*/
private float[] getBasebandFilter()
{
//Attempt to reuse a cached (ie already-designed) filter if available
float[] filter = mBasebandFilters.get(getSampleRate());
// String directory = "D:\\DQPSK Equalizer Research - P25\\"; //Windows
String directory = "/media/denny/T9/DQPSK Equalizer Research - P25/"; //Linux
if(filter == null)
// String file = directory + "P25-S2-LSM-20241225_040119_460500000_CNYICC_Onondaga_Onondaga_CC_0_baseband.wav";
// String file = directory + "P25-S4-LSM-TCH-Data-20250105_141051_453587500_CNYICC_Onondaga_T-Onondaga_CC_38_baseband.wav";
// String file = directory + "P25-S5-LSM-CCH-20250923_034304_460500000_CNYICC_Onondaga_Onondaga-CC_0_baseband.wav";
String file = directory + "P25-S6-LSM-CCH-20251025_071546_460500000_CNYICC_Onondaga_Onondaga-CC_0_baseband.wav";
// String file = directory + "P25-S3-C4FM-20241225_040459_152517500_NYSEG_Onondaga_Control_30_baseband.wav";
boolean autoReplay = false;
P25P1DecoderLSM decoder = new P25P1DecoderLSM();
decoder.start();
UserPreferences userPreferences = new UserPreferences();
P25P1AudioModule audio1 = new P25P1AudioModule(userPreferences, new AliasList(""));
decoder.setMessageListener(new Listener<>()
{
filter = FilterFactory.getLowPass(getSampleRate(), 7250, 8000, 60,
WindowType.HANN, true);
private long mBitCounter = 1;
private int mBitErrorCounter;
private int mValidMessageCounter;
private int mTotalMessageCounter;
private int mMessageCounter = 1;
mBasebandFilters.put(getSampleRate(), filter);
@Override
public void receive(IMessage iMessage)
{
int errors = 0;
audio1.receive(iMessage);
if(iMessage instanceof P25P1Message message)
{
mBitCounter += 288;
if(message.getMessage() != null)
{
errors = message.getMessage().getCorrectedBitCount();
}
mTotalMessageCounter++;
if(mTotalMessageCounter == 492)
{
int a = 0;
}
if(message.isValid())
{
mBitErrorCounter += Math.max(errors, 0);
mValidMessageCounter++;
}
}
double bitErrorRate = (double)mBitErrorCounter / (double)mBitCounter * 100.0;
boolean logEverything = true;
boolean logIdles = true;
if(logEverything)
{
if(iMessage.toString().contains("PLACEHOLDER"))
{
int a = 0;
}
if(iMessage instanceof SyncLossMessage)
{
System.out.println(" >> MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
else
{
System.out.println(++mMessageCounter + " >> MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
}
else
{
if(logIdles && iMessage instanceof SyncLossMessage)
{
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
}
}
}
});
try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay))
{
source.setListener(iNativeBuffer -> {
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
while(it.hasNext())
{
decoder.receive(it.next());
}
});
source.start();
decoder.setSampleRate(source.getSampleRate());
while(true)
{
source.next(2048, true);
}
}
catch(IOException ioe)
{
LOGGER.error("Error", ioe);
}
return filter;
LOGGER.info("Finished");
}
}

View File

@@ -1,127 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.psk.DQPSKGardnerDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
public class P25P1DecoderLSMInstrumented extends P25P1DecoderLSM
{
private Listener<Double> mPLLPhaseErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mComplexSymbolListener;
private Listener<ComplexSamples> mFilteredSymbolListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
/**
* P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and
* a gardner timing error detector.
*
* Instrumented decoder instance.
*/
public P25P1DecoderLSMInstrumented()
{
}
/**
* Primary method for receiving incoming channel samples
*/
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(samples.timestamp());
//The filter will decrement the user count when finished
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}
/**
* Overrides this method so we can correctly configure for instrumented operations
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
mQPSKDemodulator = instrumented;
instrumented.setComplexSymbolListener(mComplexSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
instrumented.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
}
public void setComplexSymbolListener(Listener<Complex> listener)
{
mComplexSymbolListener = listener;
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
}
public void setFilteredBufferListener(Listener<ComplexSamples> listener)
{
mFilteredSymbolListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
}
}

View File

@@ -186,7 +186,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
private static final Logger LOGGER = LoggerFactory.getLogger(P25P1DecoderState.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
private final Channel mChannel;
private final P25P1Decoder.Modulation mModulation;
private final Modulation mModulation;
private final PatchGroupManager mPatchGroupManager = new PatchGroupManager();
private final P25P1NetworkConfigurationMonitor mNetworkConfigurationMonitor;
private final Listener<ChannelEvent> mChannelEventListener;
@@ -229,7 +229,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
/**
* Modulation type for the decoder
*/
public P25P1Decoder.Modulation getModulation()
public Modulation getModulation()
{
return mModulation;
}
@@ -294,7 +294,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
case LOGICAL_LINK_DATA_UNIT_2:
processLDU(message);
break;
case PACKET_DATA_UNIT:
case PACKET_DATA_UNIT_BLOCK_1:
processPDU(message);
break;
case SUBNETWORK_DEPENDENT_CONVERGENCE_PROTOCOL:

View File

@@ -0,0 +1,393 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.gui.viewer.symbol.SymbolViewerFX;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Demodulates filtered LSM I/Q samples to feed message framer for sync detection and framing.
*/
public class P25P1DemodulatorLSM
{
private static final float HALF_PI = (float)(Math.PI / 2.0);
private static final float TWO_PI = (float)(Math.PI * 2.0);
private static final float MAX_PLL = (float)(Math.PI / 3.0); //+/- 800 Hz
private static final float OBJECTIVE_MAGNITUDE = 1.0f;
private static final int SYMBOL_RATE = 4800;
private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300);
private final FeedbackDecoder mFeedbackDecoder;
private final P25P1MessageFramer mMessageFramer;
private SymbolViewerFX mDebugSymbolViewer;
private double mSamplePoint;
private double mSamplesPerSymbol;
private double mSamplesPerHalfSymbol;
private float[] mBufferI;
private float[] mBufferQ;
private float mPLL = 0f;
private float mSampleGain = 1f;
private float mPreviousMiddleI, mPreviousMiddleQ, mPreviousCurrentI, mPreviousCurrentQ, mPreviousSymbolI = 0.7f, mPreviousSymbolQ = 0.7f;
private int mBufferPointer;
private int mBufferReserve;
/**
* Constructs an instance
* @param messageFramer for receiving demodulated symbol stream and providing sync detection events.
* @param feedbackDecoder (parent) for receiving PLL carrier offset error reports.
*/
public P25P1DemodulatorLSM(P25P1MessageFramer messageFramer, FeedbackDecoder feedbackDecoder)
{
mMessageFramer = messageFramer;
mFeedbackDecoder = feedbackDecoder;
}
/**
* Reset the PLL when the tuner source either changes center frequency or adjust the PPM value.
*/
public void resetPLL()
{
mPLL = 0f;
}
/**
* Debug utility for viewing I/Q waveform, demodulated waveform and symbol decision details, sequentially.
*/
protected SymbolViewerFX getDebugSymbolViewer()
{
if(mDebugSymbolViewer == null)
{
mDebugSymbolViewer = new SymbolViewerFX();
}
return mDebugSymbolViewer;
}
/**
* Primary input method for receiving a stream of filtered, pulse-shaped samples to process into symbols.
* @param i inphase samples to process
* @param q quadrature samples to process
*/
public void process(float[] i, float[] q)
{
//Shadow copy heap member variables onto the stack
double samplePoint = mSamplePoint;
double samplesPerSymbol = mSamplesPerSymbol;
double samplesPerHalfSymbol = mSamplesPerHalfSymbol;
float pll = mPLL;
float previousSymbolI = mPreviousSymbolI;
float previousSymbolQ = mPreviousSymbolQ;
float previousMiddleI = mPreviousMiddleI;
float previousMiddleQ = mPreviousMiddleQ;
float previousCurrentI = mPreviousCurrentI;
float previousCurrentQ = mPreviousCurrentQ;
float sampleGain = mSampleGain;
int bufferPointer = mBufferPointer;
float pllGain = 0.1f;
double tedGain = samplesPerSymbol / 4;
double maxTimingAdjustment = samplesPerSymbol / 25;
double pointer, residual, timingAdjustment;
float magnitude, phaseError = 0, requiredGain, softSymbol, pllI, pllQ, pllTemp;
// float previousSymbol = 0;
float iMiddle, qMiddle, iCurrent, qCurrent, iMiddleDemodulated, qMiddleDemodulated, iSymbol, qSymbol;
int offset;
Dibit hardSymbol;
//I/Q buffers are the same length as incoming samples padded by an extra symbol length for processing space.
int bufferReserve = mBufferReserve;
int requiredLength = i.length + bufferReserve;
if(mBufferI.length != requiredLength)
{
mBufferI = Arrays.copyOf(mBufferI, requiredLength);
mBufferQ = Arrays.copyOf(mBufferQ, requiredLength);
}
//Transfer extra reserve samples from last processing iteration to the front of the I/Q buffers
System.arraycopy(mBufferI, bufferPointer, mBufferI, 0, bufferReserve);
System.arraycopy(mBufferQ, bufferPointer, mBufferQ, 0, bufferReserve);
//Copy incoming I/Q samples to the processing buffers
System.arraycopy(i, 0, mBufferI, bufferReserve, i.length);
System.arraycopy(q, 0, mBufferQ, bufferReserve, q.length);
bufferPointer = 0;
int bufferReload = mBufferI.length - bufferReserve;
while(bufferPointer < bufferReload)
{
bufferPointer++;
samplePoint--;
if(samplePoint < 1)
{
//Sample point is the middle sample, between the previous and current symbols.
iMiddle = LinearInterpolator.calculate(mBufferI[bufferPointer], mBufferI[bufferPointer + 1], samplePoint);
qMiddle = LinearInterpolator.calculate(mBufferQ[bufferPointer], mBufferQ[bufferPointer + 1], samplePoint);
//Calculate offset to next symbol.
pointer = bufferPointer + samplePoint + samplesPerHalfSymbol;
offset = (int)Math.floor(pointer);
residual = pointer - offset;
iCurrent = LinearInterpolator.calculate(mBufferI[offset], mBufferI[offset + 1], residual);
qCurrent = LinearInterpolator.calculate(mBufferQ[offset], mBufferQ[offset + 1], residual);
//Adjust sample gain based on highest magnitude and apply to both middle and current samples.
magnitude = (float)(Math.sqrt(Math.pow(iCurrent, 2.0) + Math.pow(qCurrent, 2.0)));
if(magnitude > 0 && !Float.isInfinite(magnitude))
{
requiredGain = constrain(OBJECTIVE_MAGNITUDE / magnitude, 500);
sampleGain += (requiredGain - sampleGain) * .05f;
sampleGain = Math.min(sampleGain, requiredGain);
sampleGain = Math.min(sampleGain, 500);
}
//Apply gain to the samples
iMiddle *= sampleGain;
qMiddle *= sampleGain;
iCurrent *= sampleGain;
qCurrent *= sampleGain;
//Create I/Q representation of current PLL state
pllI = (float)Math.cos(pll);
pllQ = (float)Math.sin(pll);
//Differential demodulation of middle sample
iMiddleDemodulated = (previousMiddleI * iMiddle) - (-previousMiddleQ * qMiddle);
qMiddleDemodulated = (previousMiddleI * qMiddle) + (-previousMiddleQ * iMiddle);
//Rotate middle sample by the PLL offset
pllTemp = (iMiddleDemodulated * pllI) - (qMiddleDemodulated * pllQ);
qMiddleDemodulated = (qMiddleDemodulated * pllI) + (iMiddleDemodulated * pllQ);
iMiddleDemodulated = pllTemp;
//Differential demodulation of symbol
iSymbol = (previousCurrentI * iCurrent) - (-previousCurrentQ * qCurrent);
qSymbol = (previousCurrentI * qCurrent) + (-previousCurrentQ * iCurrent);
//Rotate symbol by the PLL offset
pllTemp = (iSymbol * pllI) - (qSymbol * pllQ);
qSymbol = (qSymbol * pllI) + (iSymbol * pllQ);
iSymbol = pllTemp;
//Calculate the symbol (radians) from the I/Q values
softSymbol = (float)Math.atan2(qSymbol, iSymbol);
//Apply Gardner timing error detection and correction
timingAdjustment = ((previousSymbolI - iSymbol) * iMiddleDemodulated) + ((previousSymbolQ - qSymbol) * qMiddleDemodulated);
timingAdjustment = constrain(timingAdjustment, maxTimingAdjustment);
timingAdjustment *= tedGain;
samplePoint += timingAdjustment;
//Phase Locked Loop adjustment. Note: don't adjust PLL when soft symbol value is zero.
if(softSymbol != 0)
{
hardSymbol = toDibit(softSymbol);
phaseError = constrain(softSymbol - hardSymbol.getIdealPhase(), .3f);
pll -= (phaseError * pllGain);
pll = constrain(pll, MAX_PLL);
}
else
{
hardSymbol = Dibit.D00_PLUS_1;
}
//Message framer returns boolean if valid sync and NID were detected/decoded. Send a PLL measured
//error report up to the tuner source so that it can self correct for mistuned channel and also to
//display the measured carrier offset value in the channel display
if(mMessageFramer.processWithSoftSyncDetect(softSymbol, hardSymbol))
{
mFeedbackDecoder.processPLLError(pll, SYMBOL_RATE);
}
mDibitAssembler.receive(hardSymbol);
mFeedbackDecoder.broadcast(softSymbol);
// if(bufferPointer >= 15)
// {
// SymbolViewerFX viewer = getDebugSymbolViewer();
// CountDownLatch latch = new CountDownLatch(1);
// float[] rawI = Arrays.copyOfRange(mBufferI, bufferPointer - 15, bufferPointer + 15);
// float[] rawQ = Arrays.copyOfRange(mBufferQ, bufferPointer - 15, bufferPointer + 15);
// double middle = 15 + samplePoint;// - timingAdjustment;
// double previous = middle - samplesPerHalfSymbol;
// double current = middle + samplesPerHalfSymbol;
// double[] points = {previous, middle, current, //Timing
// previousCurrentI, iMiddle, iCurrent, //RawI
// previousCurrentQ, qMiddle, qCurrent, //RawQ
// previousSymbolI, iMiddleDemodulated, iSymbol, //DemodI
// previousSymbolQ, qMiddleDemodulated, qSymbol}; //DemodQ
// float middleSymbol = normalize((float)Math.atan2(qMiddleDemodulated, iMiddleDemodulated) + pll);
// float[] symbols = {previousSymbol, middleSymbol, softSymbol};
// float adjustedPLL = pll + (phaseError * pllGain);
// viewer.receive(samplesPerSymbol, rawI, rawQ, sampleGain, adjustedPLL, points, symbols, latch);
// try
// {
// latch.await();
// }
// catch(InterruptedException e)
// {
// throw new RuntimeException(e);
// }
// previousSymbol = softSymbol;
// }
//Shuffle the values
previousSymbolI = iSymbol;
previousSymbolQ = qSymbol;
previousMiddleI = iMiddle;
previousMiddleQ = qMiddle;
previousCurrentI = iCurrent;
previousCurrentQ = qCurrent;
//Add another symbol's worth of samples to the counter
samplePoint += samplesPerSymbol;
}
}
//Copy shadow variables back to member variables.
mPLL = pll;
mBufferPointer = bufferPointer;
mPreviousMiddleI = previousMiddleI;
mPreviousMiddleQ = previousMiddleQ;
mPreviousCurrentI = previousCurrentI;
mPreviousCurrentQ = previousCurrentQ;
mPreviousSymbolI = previousSymbolI;
mPreviousSymbolQ = previousSymbolQ;
mSampleGain = sampleGain;
mSamplePoint = samplePoint;
}
/**
* Normalizes the value in radians to the range of +/-(2 * PI)
* @param value to normalize
* @return normalized value.
*/
private static float normalize(float value)
{
if(value > Math.PI)
{
return value - TWO_PI;
}
else if(value < -Math.PI)
{
return value + TWO_PI;
}
return value;
}
/**
* Constrains value to range: -constraint to constrain
* @param value to constrain
* @param constraint as max positive value
* @return constrained value.
*/
private static double constrain(double value, double constraint)
{
if(Double.isNaN(value) || Double.isInfinite(value))
{
return 0.0;
}
else if(value > constraint)
{
return constraint;
}
return Math.max(value, -constraint);
}
/**
* Constrains value to range: -constraint to constrain
* @param value to constrain
* @param constraint as max positive value
* @return constrained value.
*/
private static float constrain(float value, float constraint)
{
if(Float.isNaN(value) || Float.isInfinite(value))
{
return 0.0f;
}
if(value > constraint)
{
return constraint;
}
return Math.max(value, -constraint);
}
/**
* Sets or updates the samples per symbol
* @param samplesPerSymbol to apply.
*/
public void setSamplesPerSymbol(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mSamplesPerHalfSymbol = samplesPerSymbol / 2.0f;
mSamplePoint = samplesPerSymbol;
mBufferReserve = (int)Math.ceil(samplesPerSymbol);
mBufferI = new float[mBufferReserve];
mBufferQ = new float[mBufferReserve];
mBufferPointer = 0;
}
/**
* Registers the listener to receive demodulated bit stream buffers.
* @param listener to register
*/
public void setBufferListener(Listener<ByteBuffer> listener)
{
mDibitAssembler.setBufferListener(listener);
}
/**
* Indicates if there is a registered buffer listener
*/
public boolean hasBufferListener()
{
return mDibitAssembler.hasBufferListeners();
}
/**
* Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
* @param softSymbol in radians.
* @return symbol decision.
*/
public static Dibit toDibit(float softSymbol)
{
if(softSymbol > 0)
{
return softSymbol > HALF_PI ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
}
else
{
return softSymbol < -HALF_PI ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
}
}
}

View File

@@ -0,0 +1,273 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUHeader;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessageFactory;
import io.github.dsheirer.module.decode.p25.reference.PDUFormat;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Phase 1 message assembler. Processes an incoming stream of demodulated dibits/symbols and tracks the expected
* message length against the actual received symbol count to indicate when the message is fully assembled.
*
* This assembler also supports fuzzy message type estimation. When the NID can't be fully error corrected, we can make
* the best guess for message type and then validate at the next sync interval that the quantity of elapsed symbols
* matches the expected message length and then either accept the estimated data unit ID, or reassign a data unit ID
* that more closely matches the quantity of elapsed symbols.
*/
public class P25P1MessageAssembler implements Listener<Dibit>
{
private static final Logger LOGGER = LoggerFactory.getLogger(P25P1MessageAssembler.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
private static final int MINIMUM_LDU_BIT_LENGTH = 1500; //actual length is 1568
private CorrectedBinaryMessage mMessage;
private P25P1DataUnitID mDataUnitID;
private int mNac;
/**
* Constructs an instance
* @param nac for the message
* @param duid for the message type
*
* Note: if the duid is PLACEHOLDER it indicates the NID did not pass error detection/correction and therefore the
* duid is suspect and we'll close out the message assembly once the next sync is detected and then we can inspect
* the previous/next duids to determine what is the logical duid that would have been transmitted in between, and
* also check the dibit count to determine the correct duid.
*/
public P25P1MessageAssembler(int nac, P25P1DataUnitID duid)
{
mNac = nac;
mDataUnitID = duid;
int length = duid.getMessageLength();
if(length < 0)
{
System.out.println("Negative message length [" + length + "] duid [" + duid + "]");
length = 0;
}
mMessage = new CorrectedBinaryMessage(length);
}
/**
* Reconfigure this assembler to continue assembling subsequent message blocks. The assembling message size is
* extended to the expected message length of the new DUID.
*
* @param duid for the subsequent message block to assemble.
*/
public void reconfigure(P25P1DataUnitID duid)
{
mDataUnitID = duid;
if(mMessage != null)
{
mMessage.setSize(duid.getMessageLength());
}
else
{
mMessage = new CorrectedBinaryMessage(duid.getMessageLength());
}
}
/**
* Forces this assembler to complete assembly of the current message. Attempts to identify
*
*
* ion by either retaining the existing data unit ID when there was a valid NID
* decoded, or updating the data unit ID based on the quantity of dibits assembled thus far.
* @param previous data unit ID for the message prior to the currently assembled message
* @param next data unit ID for the message that will be assembled next
* @return number of bits that were dropped.
*/
public int forceCompletion(P25P1DataUnitID previous, P25P1DataUnitID next)
{
// System.out.println("\n[_] FORCING COMPLETION - PREV:" + previous + " CURRENT:" + mDataUnitID + " NEXT:" + next + " RECEIVED BITS:" + getMessage().currentSize() + "/" + getMessage().size() + "\n");
if(mDataUnitID == P25P1DataUnitID.PLACE_HOLDER)
{
int length = getMessage().currentSize();
if(length <= 28)
{
mDataUnitID = P25P1DataUnitID.TERMINATOR_DATA_UNIT;
}
else
{
switch(next)
{
case LOGICAL_LINK_DATA_UNIT_1:
if(length <= 770)
{
mDataUnitID = P25P1DataUnitID.HEADER_DATA_UNIT;
}
else if(length >= MINIMUM_LDU_BIT_LENGTH)
{
mDataUnitID = P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_2;
}
break;
case LOGICAL_LINK_DATA_UNIT_2:
if(length >= MINIMUM_LDU_BIT_LENGTH)
{
mDataUnitID = P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1;
}
break;
case TRUNKING_SIGNALING_BLOCK_1:
if(length >= 195)
{
//Determine if this is a TSBK or an AMBTC PDU
CorrectedBinaryMessage candidate = TSBKMessageFactory.deinterleaveViterbiAndCrc(getMessage().getSubMessage(0, 195));
if(candidate != null && candidate.getCorrectedBitCount() >= 0)
{
PDUFormat format = PDUHeader.getFormat(candidate);
if(format == PDUFormat.UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL ||
format == PDUFormat.ALTERNATE_MULTI_BLOCK_TRUNKING_CONTROL)
{
mDataUnitID = P25P1DataUnitID.PACKET_DATA_UNIT;
}
else
{
mDataUnitID = P25P1DataUnitID.TRUNKING_SIGNALING_BLOCK_1;
}
}
else
{
mDataUnitID = P25P1DataUnitID.TRUNKING_SIGNALING_BLOCK_1;
}
}
break;
}
}
//If we're still PLACE_HOLDER, meaning we didn't resolve it using the next DUID, check the previous DUID
if(mDataUnitID == P25P1DataUnitID.PLACE_HOLDER)
{
switch(previous)
{
case LOGICAL_LINK_DATA_UNIT_1:
if(length >= MINIMUM_LDU_BIT_LENGTH)
{
mDataUnitID = P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_2;
}
break;
case LOGICAL_LINK_DATA_UNIT_2:
if(length >= MINIMUM_LDU_BIT_LENGTH)
{
mDataUnitID = P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1;
}
break;
}
}
}
if(mDataUnitID == P25P1DataUnitID.PLACE_HOLDER)
{
mDataUnitID = P25P1DataUnitID.TERMINATOR_DATA_UNIT;
}
getMessage().setSize(mDataUnitID.getMessageLength());
return getMessage().size() - getMessage().currentSize();
}
/**
* The decoded NAC or zero if the NAC hasn't yet been fully decoded.
* @return NAC
*/
public int getNAC()
{
return mNac;
}
/**
* Access the completed message.
* @return message.
*/
public CorrectedBinaryMessage getMessage()
{
return mMessage;
}
/**
* Primary input method for demodulated dibits/symbols to be appended to the message under assembly.
* @param dibit to add to the message.
*/
public void receive(Dibit dibit)
{
if(mMessage.isFull())
{
LOGGING_SUPPRESSOR.error("P25P1-Full", 3, "P25 Phase 1 message under assembly is " +
"full - can't add additional dibits.");
}
else
{
mMessage.add(dibit.getBit1(), dibit.getBit2());
}
}
/**
* Indicates if the message is fully assembled meaning that the quantity of assembled dibits matches the data unit
* ID message length.
* @return true if complete.
*/
public boolean isComplete()
{
return getMessage().isFull();
}
/**
* Data unit ID for the message under assembly. Note: this is the best guess data unit ID when the isValidNID()
* method indicates false, meaning the NID was not able to be confirmed through error detection and correction.
* @return current data unit ID.
*/
public P25P1DataUnitID getDataUnitID()
{
return mDataUnitID;
}
/**
* Reassigns the data unit ID to match a more likely data unit ID based on the number of symbols that have elapsed
* between the sync detection that triggered this message assembly and the subsequent sync detection for the
* next message.
*
* @param dataUnitID to assign.
*/
public void setDataUnitID(P25P1DataUnitID dataUnitID)
{
mMessage.setSize(dataUnitID.getMessageLength());
mDataUnitID = dataUnitID;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("NAC:" + mNac + " Assembled [").append(mMessage.pointer()).append("] bits of [" +
mDataUnitID.getMessageLength() + "] for DUID:").append(mDataUnitID.name());
return sb.toString();
}
}

View File

@@ -219,6 +219,10 @@ public class P25P1MessageProcessor implements Listener<IMessage>
dispatch(message);
}
}
else
{
dispatch(message);
}
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -97,14 +97,14 @@ public class P25P1NetworkConfigurationMonitor
private MotorolaBaseStationId mMotorolaBaseStationId;
private P25P1Decoder.Modulation mModulation;
private Modulation mModulation;
/**
* Constructs a network configuration monitor.
*
* @param modulation type used by the decoder
*/
public P25P1NetworkConfigurationMonitor(P25P1Decoder.Modulation modulation)
public P25P1NetworkConfigurationMonitor(Modulation modulation)
{
mModulation = modulation;
}

View File

@@ -1,170 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.module.decode.p25.phase1;
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;
public class P25P1SyncDetector implements Listener<Dibit>
{
/* 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 (1200Hz). 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 = 4800;
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 P25P1SyncDetector(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, P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1.getMessageLength(), 48);
mPrimarySyncDetector = new SoftSyncDetector(FrameSync.P25_PHASE1_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_PHASE1_ERROR_90_CW,
phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES);
mMatcher.add(mInversionDetector90CW);
mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES);
mMatcher.add(mInversionDetector90CCW);
mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_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_PHASE1_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 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());
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-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU2Message;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.vselp.VSELP1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.vselp.VSELP2Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,7 +57,7 @@ public class P25MessageFactory
return new LDU1Message(message, nac, timestamp);
case LOGICAL_LINK_DATA_UNIT_2:
return new LDU2Message(message, nac, timestamp);
case PACKET_HEADER_DATA_UNIT:
case PACKET_DATA_UNIT:
mLog.warn("WARNING: PDU messages must be created by the PDUMessageFactory");
return null;
case TERMINATOR_DATA_UNIT:
@@ -71,10 +69,6 @@ public class P25MessageFactory
case TRUNKING_SIGNALING_BLOCK_3:
mLog.warn("WARNING: TSBK messages must be created by the TSBKMessageFactory");
return null;
case VSELP1:
return new VSELP1Message(message, nac, timestamp);
case VSELP2:
return new VSELP2Message(message, nac, timestamp);
default:
return new UnknownP25Message(message, nac, timestamp, dataUnitID);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
package io.github.dsheirer.module.decode.p25.phase1.message.ldu;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.edac.Hamming10;
import io.github.dsheirer.edac.ReedSolomon_24_12_13_P25;
@@ -61,30 +62,30 @@ public class LDU1Message extends LDUMessage implements IFrequencyBandReceiver
private static final int[] GOLAY_WORD_STARTS = {288, 298, 308, 318, 472, 482, 492, 502, 656, 666, 676, 686,
840, 850, 860, 870, 1024, 1034, 1044, 1054, 1208, 1218, 1228, 1238};
private static final int[] CW_HEX_0 = {288, 289, 290, 291, 292, 293};
private static final int[] CW_HEX_1 = {298, 299, 300, 301, 302, 303};
private static final int[] CW_HEX_2 = {308, 309, 310, 311, 312, 313};
private static final int[] CW_HEX_3 = {318, 319, 320, 321, 322, 323};
private static final int[] CW_HEX_4 = {472, 473, 474, 475, 475, 477};
private static final int[] CW_HEX_5 = {482, 483, 484, 485, 486, 487};
private static final int[] CW_HEX_6 = {492, 493, 494, 495, 496, 497};
private static final int[] CW_HEX_7 = {502, 503, 504, 505, 506, 507};
private static final int[] CW_HEX_8 = {656, 657, 658, 659, 660, 661};
private static final int[] CW_HEX_9 = {666, 667, 668, 669, 670, 671};
private static final int[] CW_HEX_10 = {676, 677, 678, 679, 680, 681};
private static final int[] CW_HEX_11 = {686, 686, 688, 689, 690, 691};
private static final int[] RS_HEX_0 = {840, 841, 842, 843, 844, 845};
private static final int[] RS_HEX_1 = {850, 851, 852, 853, 854, 855};
private static final int[] RS_HEX_2 = {860, 861, 862, 863, 864, 865};
private static final int[] RS_HEX_3 = {870, 871, 872, 873, 874, 875};
private static final int[] RS_HEX_4 = {1024, 1025, 1026, 1027, 1028, 1029};
private static final int[] RS_HEX_5 = {1034, 1035, 1036, 1037, 1038, 1039};
private static final int[] RS_HEX_6 = {1044, 1045, 1046, 1047, 1048, 1049};
private static final int[] RS_HEX_7 = {1054, 1055, 1056, 1057, 1058, 1059};
private static final int[] RS_HEX_8 = {1208, 1209, 1210, 1211, 1212, 1213};
private static final int[] RS_HEX_9 = {1218, 1219, 1220, 1221, 1222, 1223};
private static final int[] RS_HEX_10 = {1228, 1229, 1230, 1231, 1232, 1233};
private static final int[] RS_HEX_11 = {1238, 1239, 1240, 1241, 1242, 1243};
private static final IntField CW_HEX_0 = IntField.length6(288);
private static final IntField CW_HEX_1 = IntField.length6(298);
private static final IntField CW_HEX_2 = IntField.length6(308);
private static final IntField CW_HEX_3 = IntField.length6(318);
private static final IntField CW_HEX_4 = IntField.length6(472);
private static final IntField CW_HEX_5 = IntField.length6(482);
private static final IntField CW_HEX_6 = IntField.length6(492);
private static final IntField CW_HEX_7 = IntField.length6(502);
private static final IntField CW_HEX_8 = IntField.length6(656);
private static final IntField CW_HEX_9 = IntField.length6(666);
private static final IntField CW_HEX_10 = IntField.length6(676);
private static final IntField CW_HEX_11 = IntField.length6(686);
private static final IntField RS_HEX_0 = IntField.length6(840);
private static final IntField RS_HEX_1 = IntField.length6(850);
private static final IntField RS_HEX_2 = IntField.length6(860);
private static final IntField RS_HEX_3 = IntField.length6(870);
private static final IntField RS_HEX_4 = IntField.length6(1024);
private static final IntField RS_HEX_5 = IntField.length6(1034);
private static final IntField RS_HEX_6 = IntField.length6(1044);
private static final IntField RS_HEX_7 = IntField.length6(1054);
private static final IntField RS_HEX_8 = IntField.length6(1208);
private static final IntField RS_HEX_9 = IntField.length6(1218);
private static final IntField RS_HEX_10 = IntField.length6(1228);
private static final IntField RS_HEX_11 = IntField.length6(1238);
//Reed-Solomon(24,12,13) code protects the link control word. Maximum correctable errors are: floor(13/2) = 6
private static final ReedSolomon_24_12_13_P25 REED_SOLOMON_24_12_13_P25 = new ReedSolomon_24_12_13_P25();

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,22 +14,22 @@
*
* 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.message.ldu;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.Hamming10;
import io.github.dsheirer.edac.ReedSolomon_24_16_9_P25;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LDU2Message extends LDUMessage
{
@@ -38,30 +37,30 @@ public class LDU2Message extends LDUMessage
private static final int[] GOLAY_WORD_STARTS = {288, 298, 308, 318, 472, 482, 492, 502, 656, 666, 676, 686,
840, 850, 860, 870, 1024, 1034, 1044, 1054, 1208, 1218, 1228, 1238};
private static final int[] CW_HEX_0 = {288, 289, 290, 291, 292, 293};
private static final int[] CW_HEX_1 = {298, 299, 300, 301, 302, 303};
private static final int[] CW_HEX_2 = {308, 309, 310, 311, 312, 313};
private static final int[] CW_HEX_3 = {318, 319, 320, 321, 322, 323};
private static final int[] CW_HEX_4 = {472, 473, 474, 475, 475, 477};
private static final int[] CW_HEX_5 = {482, 483, 484, 485, 486, 487};
private static final int[] CW_HEX_6 = {492, 493, 494, 495, 496, 497};
private static final int[] CW_HEX_7 = {502, 503, 504, 505, 506, 507};
private static final int[] CW_HEX_8 = {656, 657, 658, 659, 660, 661};
private static final int[] CW_HEX_9 = {666, 667, 668, 669, 670, 671};
private static final int[] CW_HEX_10 = {676, 677, 678, 679, 680, 681};
private static final int[] CW_HEX_11 = {686, 686, 688, 689, 690, 691};
private static final int[] CW_HEX_12 = {840, 841, 842, 843, 844, 845};
private static final int[] CW_HEX_13 = {850, 851, 852, 853, 854, 855};
private static final int[] CW_HEX_14 = {860, 861, 862, 863, 864, 865};
private static final int[] CW_HEX_15 = {870, 871, 872, 873, 874, 875};
private static final int[] RS_HEX_0 = {1024, 1025, 1026, 1027, 1028, 1029};
private static final int[] RS_HEX_1 = {1034, 1035, 1036, 1037, 1038, 1039};
private static final int[] RS_HEX_2 = {1044, 1045, 1046, 1047, 1048, 1049};
private static final int[] RS_HEX_3 = {1054, 1055, 1056, 1057, 1058, 1059};
private static final int[] RS_HEX_4 = {1208, 1209, 1210, 1211, 1212, 1213};
private static final int[] RS_HEX_5 = {1218, 1219, 1220, 1221, 1222, 1223};
private static final int[] RS_HEX_6 = {1228, 1229, 1230, 1231, 1232, 1233};
private static final int[] RS_HEX_7 = {1238, 1239, 1240, 1241, 1242, 1243};
private static final IntField CW_HEX_0 = IntField.length6(288);
private static final IntField CW_HEX_1 = IntField.length6(298);
private static final IntField CW_HEX_2 = IntField.length6(308);
private static final IntField CW_HEX_3 = IntField.length6(318);
private static final IntField CW_HEX_4 = IntField.length6(472);
private static final IntField CW_HEX_5 = IntField.length6(482);
private static final IntField CW_HEX_6 = IntField.length6(492);
private static final IntField CW_HEX_7 = IntField.length6(502);
private static final IntField CW_HEX_8 = IntField.length6(656);
private static final IntField CW_HEX_9 = IntField.length6(666);
private static final IntField CW_HEX_10 = IntField.length6(676);
private static final IntField CW_HEX_11 = IntField.length6(686);
private static final IntField CW_HEX_12 = IntField.length6(840);
private static final IntField CW_HEX_13 = IntField.length6(850);
private static final IntField CW_HEX_14 = IntField.length6(860);
private static final IntField CW_HEX_15 = IntField.length6(870);
private static final IntField RS_HEX_0 = IntField.length6(1024);
private static final IntField RS_HEX_1 = IntField.length6(1034);
private static final IntField RS_HEX_2 = IntField.length6(1044);
private static final IntField RS_HEX_3 = IntField.length6(1054);
private static final IntField RS_HEX_4 = IntField.length6(1208);
private static final IntField RS_HEX_5 = IntField.length6(1218);
private static final IntField RS_HEX_6 = IntField.length6(1228);
private static final IntField RS_HEX_7 = IntField.length6(1238);
//Reed-Solomon(24,16,9) code protects the encryption sync word. Maximum correctable errors are floor(9/2) = 4
private static final ReedSolomon_24_16_9_P25 REED_SOLOMON_24_16_9_P25 = new ReedSolomon_24_16_9_P25();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,17 +23,21 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.IBitErrorProvider;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.reference.Direction;
import io.github.dsheirer.module.decode.p25.reference.PDUFormat;
import io.github.dsheirer.module.decode.p25.reference.Vendor;
import io.github.dsheirer.protocol.Protocol;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Packet Data Unit header
*/
public class PDUHeader implements IBitErrorProvider
public class PDUHeader implements IBitErrorProvider, IMessage
{
private final static Logger mLog = LoggerFactory.getLogger(PDUHeader.class);
@@ -66,6 +70,30 @@ public class PDUHeader implements IBitErrorProvider
return mMessage;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public long getTimestamp()
{
return 0;
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
@Override
public int getTimeslot()
{
return 0;
}
/**
* Indicates if this header was correctly decoded and passed CCITT-16 CRC error check.
*/
@@ -184,7 +212,7 @@ public class PDUHeader implements IBitErrorProvider
sb.append(getFormat().getLabel());
sb.append(isConfirmationRequired() ? " CONFIRMED" : " UNCONFIRMED");
sb.append(" VENDOR:").append(getVendor().getLabel());
sb.append(isOutbound() ? "TO" : "FROM").append(" LLID").append(getTargetLLID());
sb.append(isOutbound() ? " TO" : " FROM").append(" LLID:").append(getTargetLLID());
return sb.toString();
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,13 +14,12 @@
*
* 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.message.pdu;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCP25;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.AMBTCHeader;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketHeader;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.response.ResponseHeader;
@@ -31,12 +29,7 @@ public class PDUHeaderFactory
{
public static PDUHeader getPDUHeader(CorrectedBinaryMessage correctedBinaryMessage)
{
//CCITT-16 can detect and correct up to 1 bit error max - 2 bit errors indicates CRC-fail
int errorCount = CRCP25.correctCCITT80(correctedBinaryMessage, 0, 80);
boolean passesCRC = errorCount < 2;
correctedBinaryMessage.incrementCorrectedBitCount(errorCount);
boolean passesCRC = correctedBinaryMessage.getCorrectedBitCount() >= 0;
PDUFormat format = PDUHeader.getFormat(correctedBinaryMessage);
switch(format)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -61,7 +61,7 @@ public class PDUMessage extends P25P1Message
@Override
public P25P1DataUnitID getDUID()
{
return P25P1DataUnitID.PACKET_DATA_UNIT;
return P25P1DataUnitID.PACKET_DATA_UNIT_BLOCK_1;
}
protected String getMessageStub()

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,11 +18,7 @@
*/
package io.github.dsheirer.module.decode.p25.phase1.message.pdu;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.trellis.ViterbiDecoder_1_2_P25;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.P25P1Interleave;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.AMBTCHeader;
@@ -72,39 +68,51 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDC
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.response.ResponseMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.isp.UMBTCTelephoneInterconnectRequestExplicitDialing;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.Opcode;
import io.github.dsheirer.module.decode.p25.reference.PDUFormat;
import java.util.BitSet;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PDUMessageFactory
{
private final static Logger mLog = LoggerFactory.getLogger(PDUMessageFactory.class);
private static final int[] BLOCK_0_UMBTC_OPCODE = {2, 3, 4, 5, 6, 7};
private static final int PDU0_BEGIN = 0;
private static final int PDU0_END = 196;
private static final ViterbiDecoder_1_2_P25 VITERBI_HALF_RATE_DECODER = new ViterbiDecoder_1_2_P25();
/**
* Creates a PDU sequence from the header
* @param nac for the system
* @param timestamp of the header message
* @param correctedBinaryMessage containing an interleaved, viterbi encoded header message.
* @return
*/
public static PDUSequence createPacketSequence(int nac, long timestamp, CorrectedBinaryMessage correctedBinaryMessage)
{
//Get deinterleaved header chunk
BitSet interleaved = correctedBinaryMessage.get(PDU0_BEGIN, PDU0_END);
CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, interleaved);
PDUHeader header = createHeader(correctedBinaryMessage);
//Decode 1/2 rate trellis encoded PDU header
CorrectedBinaryMessage viterbiDecoded = VITERBI_HALF_RATE_DECODER.decode(deinterleaved);
if(viterbiDecoded != null)
if(header != null)
{
PDUHeader header = PDUHeaderFactory.getPDUHeader(viterbiDecoded);
return new PDUSequence(header, timestamp, nac);
}
return null;
}
/**
* Creates a PDU header message
* @param message containing an interleaved, viterbi encoded message
* @return header or null.
*/
public static PDUHeader createHeader(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage decoded = TSBKMessageFactory.deinterleaveViterbiAndCrc(message.getSubMessage(0, 195));
if(decoded != null)
{
return PDUHeaderFactory.getPDUHeader(decoded);
}
return null;
}
/**
* Creates a packet sequence message from the packet sequence.
*
@@ -311,83 +319,4 @@ public class PDUMessageFactory
return new PDUSequenceMessage(pduSequence, nac, timestamp);
}
}
@Deprecated
public static PDUMessage getMessage(BinaryMessage message, P25P1DataUnitID duid, AliasList aliasList)
{
PDUFormat format = PDUFormat.fromValue(message.getInt(PDUMessage.FORMAT));
// switch(format)
// {
// case ALTERNATE_MULTI_BLOCK_TRUNKING_CONTROL:
// Vendor vendor = Vendor.fromValue(
// message.getInt(PDUMessage.VENDOR_ID));
//
// switch(vendor)
// {
// case STANDARD:
// Opcode opcode = Opcode.fromValue(message.getInt(PDUMessage.OPCODE));
//
// switch(opcode)
// {
// case OSP_ADJACENT_STATUS_BROADCAST:
// return new AdjacentStatusBroadcastExtended(message, duid, aliasList);
// case OSP_CALL_ALERT:
// return new CallAlertExtended(message, duid, aliasList);
// case OSP_GROUP_AFFILIATION_QUERY:
// return new GroupAffiliationQueryExtended(message, duid, aliasList);
// case OSP_GROUP_AFFILIATION_RESPONSE:
// return new GroupAffiliationResponseExtended(message, duid, aliasList);
// case OSP_GROUP_DATA_CHANNEL_GRANT:
// return new GroupDataChannelGrantExtended(message, duid, aliasList);
// case OSP_GROUP_VOICE_CHANNEL_GRANT:
// return new GroupVoiceChannelGrantExplicit(message, duid, aliasList);
// case OSP_INDIVIDUAL_DATA_CHANNEL_GRANT:
// return new IndividualDataChannelGrantExtended(message, duid, aliasList);
// case OSP_MESSAGE_UPDATE:
// return new MessageUpdateExtended(message, duid, aliasList);
// case OSP_NETWORK_STATUS_BROADCAST:
// return new NetworkStatusBroadcastExtended(message, duid, aliasList);
// case OSP_PROTECTION_PARAMETER_BROADCAST:
// return new ProtectionParameterBroadcast(message, duid, aliasList);
// case OSP_RFSS_STATUS_BROADCAST:
// return new RFSSStatusBroadcastExtended(message,
// duid, aliasList);
// case OSP_ROAMING_ADDRESS_UPDATE:
// return new RoamingAddressUpdateExtended(message, duid, aliasList);
// case OSP_STATUS_QUERY:
// return new StatusQueryExtended(message, duid, aliasList);
// case OSP_STATUS_UPDATE:
// return new StatusUpdateExtended(message, duid,
// aliasList);
// case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT:
// return new TelephoneInterconnectChannelGrantExplicit(message, duid, aliasList);
// case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE:
// return new TelephoneInterconnectChannelGrantUpdateExplicit(message, duid, aliasList);
// case OSP_UNIT_REGISTRATION_RESPONSE:
// return new UnitRegistrationResponseExtended(message, duid, aliasList);
// case OSP_UNIT_TO_UNIT_ANSWER_REQUEST:
// return new UnitToUnitAnswerRequestExplicit(message, duid, aliasList);
// case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT:
// return new UnitToUnitVoiceChannelGrantExtended(message, duid, aliasList);
// case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE:
// return new UnitToUnitVoiceChannelGrantUpdateExtended(message, duid, aliasList);
// default:
// break;
// }
// case MOTOROLA:
// break;
// default:
// break;
// }
//
// return new PDUMessage(message, duid, aliasList);
//
// case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
// return new PDUMessage(message, duid, aliasList);
// default:
// return new PDUMessage(message, duid, aliasList);
// }
return null;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.module.decode.p25.phase1.message.pdu;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
@@ -31,7 +32,7 @@ public class PDUSequenceMessage extends P25P1Message
public PDUSequenceMessage(PDUSequence PDUSequence, int nac, long timestamp)
{
super(null, nac, timestamp);
super(new CorrectedBinaryMessage(0), nac, timestamp);
mPDUSequence = PDUSequence;
}
@@ -50,7 +51,7 @@ public class PDUSequenceMessage extends P25P1Message
@Override
public P25P1DataUnitID getDUID()
{
return P25P1DataUnitID.PACKET_DATA_UNIT;
return P25P1DataUnitID.PACKET_DATA_UNIT_BLOCK_1;
}
@Override

View File

@@ -47,6 +47,10 @@ public abstract class AMBTCMessage extends P25P1Message implements IBitErrorProv
{
StringBuilder sb = new StringBuilder();
sb.append(super.getMessageStub());
if(!getHeader().isValid())
{
sb.append(" **CRC-FAILED**");
}
sb.append(" ").append(getHeader().getOpcode());
P25Utils.pad(sb, 30);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -62,7 +62,7 @@ public class ResponseMessage extends PDUSequenceMessage
sb.append("***CRC-FAIL*** ");
}
sb.append("PDU RESPONSE");
sb.append(" PDU RESPONSE");
if(getHeader().hasSourceLLID())
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,6 @@ package io.github.dsheirer.module.decode.p25.phase1.message.tsbk;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCP25;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.P25Utils;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
@@ -145,20 +144,18 @@ public abstract class TSBKMessage extends P25P1Message
StringBuilder sb = new StringBuilder();
sb.append(super.getMessageStub());
if(isValid())
if(!isValid())
{
sb.append(" ").append(getOpcode().getLabel());
P25Utils.pad(sb, 30);
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
sb.append(" **CRC-FAILED**");
}
else
sb.append(" ").append(getOpcode().getLabel());
P25Utils.pad(sb, 30);
if(isEncrypted())
{
sb.append("**CRC-FAILED**");
sb.append(" ENCRYPTED");
}
return sb.toString();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -135,12 +135,18 @@ public class TSBKMessageFactory
{
private final static Logger mLog = LoggerFactory.getLogger(TSBKMessageFactory.class);
private static final ViterbiDecoder_1_2_P25 VITERBI_HALF_RATE_DECODER = new ViterbiDecoder_1_2_P25();
public static final int CRC_FAIL = -1;
public static TSBKMessage create(Direction direction, P25P1DataUnitID dataUnitID,
CorrectedBinaryMessage correctedBinaryMessage, int nac, long timestamp)
/**
* Performs deinterleave, Viterbi decoding, and applies the CRC-CCITT error detection/correction.
* @param raw interleaved message
* @return null or the decoded message with CRC results set in the message where 0 or 1 bit errors is valid and
* 2 bit errors is invalid.
*/
public static CorrectedBinaryMessage deinterleaveViterbiAndCrc(CorrectedBinaryMessage raw)
{
//Get deinterleaved header chunk
CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, correctedBinaryMessage);
CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, raw);
//Decode 1/2 rate trellis encoded PDU header
CorrectedBinaryMessage message = VITERBI_HALF_RATE_DECODER.decode(deinterleaved);
@@ -154,6 +160,25 @@ public class TSBKMessageFactory
//invalid if the algorithm detects more than 1 correctable error.
int errors = CRCP25.correctCCITT80(message, 0, 80);
if(errors > 1)
{
message.setCorrectedBitCount(CRC_FAIL);
}
return message;
}
public static TSBKMessage create(Direction direction, P25P1DataUnitID dataUnitID,
CorrectedBinaryMessage correctedBinaryMessage, int nac, long timestamp)
{
CorrectedBinaryMessage message = deinterleaveViterbiAndCrc(correctedBinaryMessage);
if(message == null)
{
return null;
}
int errors = message.getCorrectedBitCount();
Vendor vendor = TSBKMessage.getVendor(message);
Opcode opcode = TSBKMessage.getOpcode(message, direction, vendor);
@@ -469,12 +494,11 @@ public class TSBKMessageFactory
break;
}
if(tsbk != null && errors > 1)
if(tsbk != null && errors < 0)
{
tsbk.setValid(false);
}
return tsbk;
}
}

View File

@@ -1,59 +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.phase1.message.vselp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import java.util.Collections;
import java.util.List;
public class VSELP1Message extends P25P1Message
{
/**
* Motorola VSELP audio message 1 - not implemented.
*/
public VSELP1Message(CorrectedBinaryMessage message, int nac, long timestamp)
{
super(message, nac, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public P25P1DataUnitID getDUID()
{
return P25P1DataUnitID.VSELP1;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.EMPTY_LIST;
}
}

View File

@@ -1,59 +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.phase1.message.vselp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import java.util.Collections;
import java.util.List;
public class VSELP2Message extends P25P1Message
{
/**
* Motorola VSELP audio message - not implemented.
*/
public VSELP2Message(CorrectedBinaryMessage message, int nac, long timestamp)
{
super(message, nac, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public P25P1DataUnitID getDUID()
{
return P25P1DataUnitID.VSELP2;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.EMPTY_LIST;
}
}

View File

@@ -17,23 +17,34 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.sync.visualizer;
package io.github.dsheirer.module.decode.p25.phase1.sync;
import java.util.concurrent.CountDownLatch;
import io.github.dsheirer.dsp.symbol.Dibit;
/**
* Interface for receiving sync detection results from a demodulated I/Q stream
* P25P1 sync detector that processes hard symbol (ie dibit) decisions.
*/
public interface ISyncResultsListener
public class P25P1HardSyncDetector extends P25P1SyncDetector
{
private static final int MAXIMUM_BIT_ERROR = 4;
private long mValue;
/**
* Receive results
* @param symbols for the detected sync
* @param sync pattern symbols (ideal)
* @param samples demodulated samples
* @param syncIntervals for timing of each symbol in the samples array
* @param label to display in the UI
* @param release that pauses execution until the user acknowledges the displayed sync results. Decrement the latch to release.
* Constructs an instance
*/
void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, String label, CountDownLatch release);
public P25P1HardSyncDetector()
{
}
/**
* Processes the dibit.
*
* @param dibit to process
* @return true if a sync pattern is detected.
*/
public boolean process(Dibit dibit)
{
mValue = (Long.rotateLeft(mValue, 2) & SYNC_MASK) + dibit.getValue();
return Long.bitCount(mValue ^ SYNC_PATTERN) <= MAXIMUM_BIT_ERROR;
}
}

View File

@@ -0,0 +1,71 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.sync;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Arrays;
/**
* P25P1 Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream.
*/
public abstract class P25P1SoftSyncDetector extends P25P1SyncDetector
{
protected static final float MAX_POSITIVE = Dibit.D01_PLUS_3.getIdealPhase();
protected static final float MAX_NEGATIVE = Dibit.D11_MINUS_3.getIdealPhase();
protected static final float[] SYNC_PATTERN_SYMBOLS = syncPatternToSymbols();
protected float[] mSymbols = new float[48]; //2x longer than 48-bit/24-dibit DMR sync patterns.
protected int mSymbolPointer = 0;
public P25P1SoftSyncDetector()
{
}
/**
* 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 24 soft
* symbols that include this recent soft symbol.
*
* @param dibitSymbol to process that represents the soft demodulated symbol value in phase angle/radians.
* @return correlation score.
*/
public float process(float dibitSymbol)
{
mSymbols[mSymbolPointer] = dibitSymbol;
mSymbols[mSymbolPointer + 24] = dibitSymbol;
mSymbolPointer++;
mSymbolPointer %= 24;
return calculate();
}
}

View File

@@ -0,0 +1,59 @@
/*
* *****************************************************************************
* 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.phase1.sync;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating P25 Phase 1 Soft Sync Detectors
*/
public class P25P1SoftSyncDetectorFactory
{
private static final Logger mLog = LoggerFactory.getLogger(P25P1SoftSyncDetectorFactory.class);
/**
* Creates the implementation using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static P25P1SoftSyncDetector getDetector()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.P25P1_SOFT_SYNC_DETECTOR);
switch(implementation)
{
case VECTOR_SIMD_64:
return new P25P1SoftSyncDetectorVector64();
case VECTOR_SIMD_128:
return new P25P1SoftSyncDetectorVector128();
case VECTOR_SIMD_256:
return new P25P1SoftSyncDetectorVector256();
case VECTOR_SIMD_512:
return new P25P1SoftSyncDetectorVector512();
case SCALAR:
default:
return new P25P1SoftSyncDetectorScalar();
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* 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.phase1.sync;
/**
* Scalar implementation of P25 Phase 1 Soft Sync Detector.
*/
public class P25P1SoftSyncDetectorScalar extends P25P1SoftSyncDetector
{
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
float symbol;
float score = 0;
for(int x = 0; x < 24; x++)
{
symbol = mSymbols[mSymbolPointer + x];
score += SYNC_PATTERN_SYMBOLS[x] * symbol;
}
return score;
}
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* 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.phase1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 128 implementation of P25 Phase 1 Soft Sync Detector.
*/
public class P25P1SoftSyncDetectorVector128 extends P25P1SoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
for(int x = 0; x < 24; x += VECTOR_SPECIES.length())
{
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator);
}
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* 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.phase1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 256 implementation of P25 Phase 1 Soft Sync Detector.
*/
public class P25P1SoftSyncDetectorVector256 extends P25P1SoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
for(int x = 0; x < 24; x += VECTOR_SPECIES.length())
{
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator);
}
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,55 @@
/*
* *****************************************************************************
* 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.phase1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 512 implementation of P25 Phase 1 Soft Sync Detector.
*/
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;
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
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, SYNC_PATTERN_SYMBOLS, 0), accumulator);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8, mask)
.fma(FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, 8), accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* 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.phase1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 64 implementation of P25 Phase 1 Soft Sync Detector.
*/
public class P25P1SoftSyncDetectorVector64 extends P25P1SoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
for(int x = 0; x < 24; x += VECTOR_SPECIES.length())
{
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator);
}
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,79 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.sync;
import io.github.dsheirer.dsp.symbol.Dibit;
/**
* Base implementation of P25 Phase 1 sync detector.
*/
public abstract class P25P1SyncDetector
{
public static final long SYNC_MASK = 0xFFFFFFFFFFFFL;
public static final long SYNC_PATTERN = 0x5575F5FF77FFL;
/**
* Converts the P25P1 sync pattern to a float array of ideal phase values for each Dibit to use for correlation
* against a stream of transmitted symbol phases.
* @return symbols array representing the sync pattern.
*/
public static float[] syncPatternToSymbols()
{
float[] symbols = new float[24];
Dibit[] dibits = syncPatternToDibits();
for(int x = 0; x < 24; x++)
{
symbols[x] = dibits[x].getIdealPhase();
}
return symbols;
}
/**
* Converts the P25P1 sync pattern to an array of dibit symbols. Note: only the +3 and -3 dibit symbols are used in
* sync patterns.
* @return dibit symbols array representing the sync pattern.
*/
public static Dibit[] syncPatternToDibits()
{
Dibit[] dibits = new Dibit[24];
long mask = 3;
long dibitValue;
for(int x = 0; x < 24; x++)
{
dibitValue = (SYNC_PATTERN & mask) >> (2 * x);
if(dibitValue == 1)
{
dibits[23 - x] = Dibit.D01_PLUS_3;
}
else
{
dibits[23 - x] = Dibit.D11_MINUS_3;
}
mask = mask << 2;
}
return dibits;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,6 +56,12 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
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

View File

@@ -1,141 +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.dsp.psk.DQPSKGardnerDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
public class P25P2DecoderHDQPSKInstrumented extends P25P2DecoderHDQPSK
{
private Listener<Double> mPLLPhaseErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mComplexSymbolListener;
private Listener<ComplexSamples> mFilteredSymbolListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
/**
* Instrumented version of the P25 C4FM decoder that supports registering listeners to provide access to data as
* it is being processed by the decoder.
*/
public P25P2DecoderHDQPSKInstrumented(DecodeConfigP25Phase2 decodeConfigP25Phase2)
{
super((decodeConfigP25Phase2));
}
/**
* Demodulator
*/
public DQPSKGardnerDemodulatorInstrumented getDemodulator()
{
return (DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator;
}
/**
* Primary method for processing incoming complex sample buffers
* @param samples containing channelized complex samples
*/
@Override
public void receive(ComplexSamples samples)
{
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(samples);
}
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}
/**
* Instrumented interpolating sample buffer
*/
public InterpolatingSampleBufferInstrumented getSampleBuffer()
{
return (InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer;
}
/**
* Overrides this method so we can correctly configure for instrumented operations
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer =
new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
mQPSKDemodulator = instrumented;
instrumented.setComplexSymbolListener(mComplexSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
instrumented.setSymbolListener(getDibitBroadcaster());
getDibitBroadcaster().addListener(mMessageFramer);
}
public void setComplexSymbolListener(Listener<Complex> listener)
{
mComplexSymbolListener = listener;
getDemodulator().setComplexSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
getDemodulator().setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
getDemodulator().setPLLFrequencyListener(listener);
}
public void setFilteredBufferListener(Listener<ComplexSamples> listener)
{
mFilteredSymbolListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
getDemodulator().setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
getDemodulator().setSamplesPerSymbolListener(listener);
}
}

View File

@@ -1,55 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.tait;
import io.github.dsheirer.dsp.afsk.AFSK1200DecoderInstrumented;
import io.github.dsheirer.gui.instrument.chart.IInstrumentedAFSK1200Decoder;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Instrumented version of the AFSK-1200 decoder for use with DemodulatorViewerFX
*/
public class Tait1200DecoderInstrumented extends Tait1200Decoder implements IInstrumentedAFSK1200Decoder
{
public SimpleIntegerProperty bufferCount = new SimpleIntegerProperty();
public Tait1200DecoderInstrumented()
{
super(new AFSK1200DecoderInstrumented(false));
}
@Override
public SimpleIntegerProperty getBufferCountProperty()
{
return bufferCount;
}
public AFSK1200DecoderInstrumented getAFSK1200Decoder()
{
return (AFSK1200DecoderInstrumented)getDecoder();
}
@Override
public void receive(float[] realBuffer)
{
super.receive(realBuffer);
bufferCount.setValue(bufferCount.intValue() + 1);
}
}