mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
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:
@@ -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,6 +25,7 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.SingleChannelState;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
@@ -164,8 +165,9 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
List<MessagePackage> messagePackages = new ArrayList<>();
|
||||
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
|
||||
P25P1MessageFramer messageFramer = new P25P1MessageFramer();
|
||||
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
|
||||
|
||||
Broadcaster<IMessage> messageBroadcaster = new Broadcaster<>();
|
||||
messageFramer.setListener(messageBroadcaster);
|
||||
messageBroadcaster.addListener(messageProcessor);
|
||||
@@ -229,7 +231,13 @@ public class P25P1Viewer extends VBox
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
messageFramer.receive(buffer);
|
||||
for(byte value : buffer.array())
|
||||
{
|
||||
for(int x = 0; x <= 3; x++)
|
||||
{
|
||||
messageFramer.processWithHardSyncDetect(Dibit.parse(value, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.symbol;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public interface ISymbolResultsListener
|
||||
{
|
||||
void receive(double samplesPerSymbol, float[] rawI, float[] rawQ, float sampleGain, float pll,
|
||||
double[] points, float[] symbols, CountDownLatch release);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.symbol;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
|
||||
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.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 SymbolViewPanel extends VBox implements ISymbolResultsListener
|
||||
{
|
||||
private static final double TWO_PI = Math.PI * 2.0;
|
||||
private final NumberAxis mRawTiming = new NumberAxis();
|
||||
private final NumberAxis mRawValue = new NumberAxis(-1.1, 1.1, .1);
|
||||
private final NumberAxis mDemodulatedTiming = new NumberAxis();
|
||||
private final NumberAxis mDemodulatedValue = new NumberAxis(-1.1, 1.1, .1);
|
||||
private final NumberAxis mSymbolTiming = new NumberAxis();
|
||||
private final NumberAxis mSymbolValue = new NumberAxis(-Math.PI, Math.PI, Math.PI / 4.0);
|
||||
private final LineChart<Number, Number> mRawChart = new LineChart<>(mRawTiming, mRawValue);
|
||||
private final LineChart<Number, Number> mDemodulatedChart = new LineChart<>(mDemodulatedTiming, mDemodulatedValue);
|
||||
private final LineChart<Number, Number> mSymbolChart = new LineChart<>(mSymbolTiming, mSymbolValue);
|
||||
private final Button mReleaseButton = new Button("Next Symbol");
|
||||
private final Label mDisplayText = new Label();
|
||||
private CountDownLatch mRelease;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public SymbolViewPanel()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
mRawTiming.setLabel("Raw Sample Timing");
|
||||
mRawValue.setLabel("Raw Sample Value (+/- 1.0)");
|
||||
mDemodulatedTiming.setLabel("Demodulated Sample Timing");
|
||||
mDemodulatedValue.setLabel("Demodulated Sample Value (+/- 1.0)");
|
||||
mSymbolTiming.setLabel("Symbol Timing");
|
||||
mSymbolValue.setLabel("Symbol Value (+/- PI)");
|
||||
|
||||
mReleaseButton.setOnAction(e -> {
|
||||
if(mRelease != null)
|
||||
{
|
||||
mRelease.countDown();
|
||||
mReleaseButton.setDisable(true);
|
||||
}
|
||||
});
|
||||
|
||||
mRawChart.setAnimated(false);
|
||||
mDemodulatedChart.setAnimated(false);
|
||||
mSymbolChart.setAnimated(false);
|
||||
|
||||
VBox.setVgrow(mRawChart, Priority.ALWAYS);
|
||||
VBox.setVgrow(mDemodulatedChart, Priority.ALWAYS);
|
||||
VBox.setVgrow(mSymbolChart, Priority.ALWAYS);
|
||||
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.BASELINE_LEFT);
|
||||
buttons.setSpacing(10);
|
||||
buttons.getChildren().addAll(mReleaseButton, mDisplayText);
|
||||
VBox.setVgrow(buttons, Priority.NEVER);
|
||||
|
||||
getChildren().addAll(buttons, mRawChart, mDemodulatedChart, mSymbolChart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(double samplesPerSymbol, float[] rawI, float[] rawQ, float sampleGain, float pll,
|
||||
double[] points, float[] symbolSequence, CountDownLatch release)
|
||||
{
|
||||
mRelease = release;
|
||||
mReleaseButton.setDisable(release == null);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
XYChart.Series<Number, Number> rawSeriesI = new XYChart.Series<>();
|
||||
rawSeriesI.setName("Raw Samples");
|
||||
XYChart.Series<Number, Number> rawSeriesQ = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> rawTiming = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> rawDecisionI = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> rawDecisionQ = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> demodulatedSeriesI = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> demodulatedSeriesQ = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> demodulatedTiming = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> demodulatedDecisionI = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> demodulatedDecisionQ = new XYChart.Series<>();
|
||||
|
||||
XYChart.Series<Number, Number> symbolsRaw = new XYChart.Series<>();
|
||||
symbolsRaw.setName("Symbols Raw");
|
||||
XYChart.Series<Number, Number> symbolsPLL = new XYChart.Series<>();
|
||||
symbolsPLL.setName("Symbols PLL");
|
||||
XYChart.Series<Number, Number> symbolTiming = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolP3 = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolP1 = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolM1 = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolM3 = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolsWithPLL = new XYChart.Series<>();
|
||||
XYChart.Series<Number, Number> symbolsWithoutPLL = new XYChart.Series<>();
|
||||
|
||||
float pllI = (float)Math.cos(pll);
|
||||
float pllQ = (float)Math.sin(pll);
|
||||
float pllTemp = 0;
|
||||
|
||||
double spsMax = Math.ceil(samplesPerSymbol);
|
||||
double fractional = spsMax - samplesPerSymbol;
|
||||
for(int x = 0; x < rawI.length; x++)
|
||||
{
|
||||
rawSeriesI.getData().add(new XYChart.Data<>(x, rawI[x] * sampleGain));
|
||||
rawSeriesQ.getData().add(new XYChart.Data<>(x, rawQ[x] * sampleGain));
|
||||
|
||||
if(x >= spsMax)
|
||||
{
|
||||
int offset = x - (int)spsMax;
|
||||
float previousI = LinearInterpolator.calculate(rawI[offset] * sampleGain, rawI[offset + 1] * sampleGain, fractional);
|
||||
float previousQ = LinearInterpolator.calculate(rawQ[offset] * sampleGain, rawQ[offset + 1] * sampleGain, fractional);
|
||||
float currentI = rawI[x] * sampleGain;
|
||||
float currentQ = rawQ[x] * sampleGain;
|
||||
float demodulatedI = (previousI * currentI) - (-previousQ * currentQ);
|
||||
float demodulatedQ = (previousI * currentQ) + (-previousQ * currentI);
|
||||
|
||||
//Rotate by the PLL
|
||||
pllTemp = (demodulatedI * pllI) - (demodulatedQ * pllQ);
|
||||
demodulatedQ = (demodulatedQ * pllI) + (demodulatedI * pllQ);
|
||||
demodulatedI = pllTemp;
|
||||
|
||||
demodulatedSeriesI.getData().add(new XYChart.Data<>(x, demodulatedI));
|
||||
demodulatedSeriesQ.getData().add(new XYChart.Data<>(x, demodulatedQ));
|
||||
symbolsRaw.getData().add(new XYChart.Data<>(x, Math.atan2(demodulatedQ, demodulatedI)));
|
||||
// symbolsPLL.getData().add(new XYChart.Data<>(x, normalize(Math.atan2(demodulatedQ, demodulatedI) + pll)));
|
||||
}
|
||||
}
|
||||
|
||||
rawTiming.getData().add(new XYChart.Data<>(0, -1));
|
||||
demodulatedTiming.getData().add(new XYChart.Data<>(0, -1));
|
||||
symbolTiming.getData().add(new XYChart.Data<>(0, -Math.PI));
|
||||
|
||||
for(int x = 0; x < 3; x++)
|
||||
{
|
||||
rawTiming.getData().add(new XYChart.Data<>(points[x], -1));
|
||||
rawTiming.getData().add(new XYChart.Data<>(points[x], 1));
|
||||
rawTiming.getData().add(new XYChart.Data<>(points[x], -1));
|
||||
demodulatedTiming.getData().add(new XYChart.Data<>(points[x], -1));
|
||||
demodulatedTiming.getData().add(new XYChart.Data<>(points[x], 1));
|
||||
demodulatedTiming.getData().add(new XYChart.Data<>(points[x], -1));
|
||||
symbolTiming.getData().add(new XYChart.Data<>(points[x], -Math.PI));
|
||||
symbolTiming.getData().add(new XYChart.Data<>(points[x], Math.PI));
|
||||
symbolTiming.getData().add(new XYChart.Data<>(points[x], -Math.PI));
|
||||
}
|
||||
|
||||
rawTiming.getData().add(new XYChart.Data<>(rawI.length - 1, -1));
|
||||
demodulatedTiming.getData().add(new XYChart.Data<>(rawI.length - 1, -1));
|
||||
symbolTiming.getData().add(new XYChart.Data<>(rawI.length - 1, -Math.PI));
|
||||
|
||||
rawDecisionI.getData().add(new XYChart.Data<>(points[0], points[3]));
|
||||
rawDecisionI.getData().add(new XYChart.Data<>(points[1], points[4]));
|
||||
rawDecisionI.getData().add(new XYChart.Data<>(points[2], points[5]));
|
||||
|
||||
rawDecisionQ.getData().add(new XYChart.Data<>(points[0], points[6]));
|
||||
rawDecisionQ.getData().add(new XYChart.Data<>(points[1], points[7]));
|
||||
rawDecisionQ.getData().add(new XYChart.Data<>(points[2], points[8]));
|
||||
|
||||
demodulatedDecisionI.getData().add(new XYChart.Data<>(points[0], points[9]));
|
||||
demodulatedDecisionI.getData().add(new XYChart.Data<>(points[1], points[10]));
|
||||
demodulatedDecisionI.getData().add(new XYChart.Data<>(points[2], points[11]));
|
||||
|
||||
demodulatedDecisionQ.getData().add(new XYChart.Data<>(points[0], points[12]));
|
||||
demodulatedDecisionQ.getData().add(new XYChart.Data<>(points[1], points[13]));
|
||||
demodulatedDecisionQ.getData().add(new XYChart.Data<>(points[2], points[14]));
|
||||
|
||||
symbolP3.getData().add(new XYChart.Data<>(0, 3 * Math.PI / 4));
|
||||
symbolP1.getData().add(new XYChart.Data<>(0, Math.PI / 4));
|
||||
symbolM1.getData().add(new XYChart.Data<>(0, -Math.PI / 4));
|
||||
symbolM3.getData().add(new XYChart.Data<>(0, -3 * Math.PI / 4));
|
||||
symbolP3.getData().add(new XYChart.Data<>(rawI.length - 1, 3 * Math.PI / 4));
|
||||
symbolP1.getData().add(new XYChart.Data<>(rawI.length - 1, Math.PI / 4));
|
||||
symbolM1.getData().add(new XYChart.Data<>(rawI.length - 1, -Math.PI / 4));
|
||||
symbolM3.getData().add(new XYChart.Data<>(rawI.length - 1, -3 * Math.PI / 4));
|
||||
|
||||
symbolsWithPLL.getData().add((new XYChart.Data<>(points[0], symbolSequence[0])));
|
||||
// symbolsWithPLL.getData().add((new XYChart.Data<>(points[1], symbolSequence[1])));
|
||||
symbolsWithPLL.getData().add((new XYChart.Data<>(points[2], symbolSequence[2])));
|
||||
// symbolsWithoutPLL.getData().add((new XYChart.Data<>(points[0], normalize(symbolSequence[0] - pll))));
|
||||
// symbolsWithoutPLL.getData().add((new XYChart.Data<>(points[1], normalize(symbolSequence[1] - pll))));
|
||||
// symbolsWithoutPLL.getData().add((new XYChart.Data<>(points[2], normalize(symbolSequence[2] - pll))));
|
||||
|
||||
mRawChart.getData().clear();
|
||||
mRawChart.getData().add(rawSeriesI);
|
||||
mRawChart.getData().add(rawSeriesQ);
|
||||
mRawChart.getData().add(rawTiming);
|
||||
mRawChart.getData().add(rawDecisionI);
|
||||
mRawChart.getData().add(rawDecisionQ);
|
||||
mDemodulatedChart.getData().clear();
|
||||
mDemodulatedChart.getData().add(demodulatedSeriesI);
|
||||
mDemodulatedChart.getData().add(demodulatedSeriesQ);
|
||||
mDemodulatedChart.getData().add(demodulatedTiming);
|
||||
mDemodulatedChart.getData().add(demodulatedDecisionI);
|
||||
mDemodulatedChart.getData().add(demodulatedDecisionQ);
|
||||
mSymbolChart.getData().clear();
|
||||
mSymbolChart.getData().add(symbolsRaw);
|
||||
// mSymbolChart.getData().add(symbolsPLL);
|
||||
mSymbolChart.getData().add(symbolTiming);
|
||||
mSymbolChart.getData().add(symbolP3);
|
||||
mSymbolChart.getData().add(symbolP1);
|
||||
mSymbolChart.getData().add(symbolM1);
|
||||
mSymbolChart.getData().add(symbolM3);
|
||||
mSymbolChart.getData().add(symbolsWithPLL);
|
||||
// mSymbolChart.getData().add(symbolsWithoutPLL);
|
||||
mDisplayText.setText("PLL: " + pll + " Sample Gain: " + sampleGain);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the value in radians to the range of +/-(2 * PI)
|
||||
* @param value to normalize
|
||||
* @return normalized value.
|
||||
*/
|
||||
private static double normalize(double value)
|
||||
{
|
||||
if(value > Math.PI)
|
||||
{
|
||||
return value - TWO_PI;
|
||||
}
|
||||
else if(value < -Math.PI)
|
||||
{
|
||||
return value + TWO_PI;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.symbol;
|
||||
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
/**
|
||||
* Utility for viewing sync detection results
|
||||
*/
|
||||
public class SymbolViewerFX implements ISymbolResultsListener
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SymbolViewerFX.class.getName());
|
||||
private ISymbolResultsListener mListener;
|
||||
|
||||
public SymbolViewerFX()
|
||||
{
|
||||
initUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(double samplesPerSymbol, float[] rawI, float[] rawQ, float sampleGain, float pll,
|
||||
double[] points, float[] symbols, CountDownLatch releasee)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(samplesPerSymbol, rawI, rawQ, sampleGain, pll, points, symbols, releasee);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the viewer UI
|
||||
*/
|
||||
private void initUI()
|
||||
{
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
final JFXPanel fxPanel = new JFXPanel();
|
||||
Platform.runLater(() -> {
|
||||
Platform.setImplicitExit(false);
|
||||
SymbolViewPanel viewer = new SymbolViewPanel();
|
||||
mListener = viewer;
|
||||
Scene scene = new Scene(viewer, 1400, 1000);
|
||||
|
||||
// URL resource = getClass().getResource("/sdrtrunk_style.css");
|
||||
//
|
||||
// if(resource != null)
|
||||
// {
|
||||
// scene.getStylesheets().add(resource.toExternalForm());
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
|
||||
// }
|
||||
|
||||
fxPanel.setScene(scene);
|
||||
fxPanel.setVisible(true);
|
||||
|
||||
JFrame frame = new JFrame();
|
||||
frame.setTitle("Symbol Results Viewer");
|
||||
frame.setContentPane(fxPanel);
|
||||
frame.setSize(1400, 1400);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.sync;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* Interface for receiving sync detection results from a demodulated I/Q stream
|
||||
*/
|
||||
public interface ISyncResultsListener
|
||||
{
|
||||
/**
|
||||
* 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 equalizerBalance balance for the equalizer
|
||||
* @param equalizerGain gain for the equalizer
|
||||
* @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.
|
||||
*/
|
||||
void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, float equalizerBalance,
|
||||
float equalizerGain, String label, CountDownLatch release);
|
||||
|
||||
/**
|
||||
* Post a symbol value to the listener
|
||||
* @param symbol value in range +/- PI
|
||||
*/
|
||||
void symbol(float symbol);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.sync;
|
||||
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
/**
|
||||
* Utility for viewing sync detection results
|
||||
*/
|
||||
public class SyncResultsViewer implements ISyncResultsListener
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SyncResultsViewer.class.getName());
|
||||
private ISyncResultsListener mListener;
|
||||
|
||||
public SyncResultsViewer()
|
||||
{
|
||||
initUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* View sync detection results.
|
||||
* @param symbols that were detected
|
||||
* @param sync that was detected
|
||||
* @param samples for the symbols
|
||||
* @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, float equalizerBalance,
|
||||
float equalizerGain, String label, CountDownLatch release)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(symbols, samples, sync, syncIntervals, equalizerBalance, equalizerGain, label, release);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void symbol(float symbol)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.symbol(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// URL resource = getClass().getResource("/sdrtrunk_style.css");
|
||||
//
|
||||
// if(resource != null)
|
||||
// {
|
||||
// scene.getStylesheets().add(resource.toExternalForm());
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
|
||||
// }
|
||||
|
||||
fxPanel.setScene(scene);
|
||||
fxPanel.setVisible(true);
|
||||
|
||||
JFrame frame = new JFrame();
|
||||
frame.setTitle("Sync Results Viewer");
|
||||
frame.setContentPane(fxPanel);
|
||||
frame.setSize(1400, 1400);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.gui.viewer.sync;
|
||||
|
||||
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 int SYMBOL_CHART_SERIES_COUNT = 2000;
|
||||
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(-3.5, 3.5, .5);
|
||||
private final NumberAxis mSymbolTiming = new NumberAxis();
|
||||
private final NumberAxis mSymbolPhase = new NumberAxis(-Math.PI, Math.PI, Math.PI / 2);
|
||||
private final XYChart.Series<Number, Number> mSymbolSeries = new XYChart.Series<>();
|
||||
private final XYChart.Series<Number, Number> mSymbolSeriesMarker = new XYChart.Series<>();
|
||||
private final ScatterChart<Number, Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
|
||||
private final LineChart<Number, Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
|
||||
private final ScatterChart<Number, Number> mSymbolChart = new ScatterChart<>(mSymbolTiming, mSymbolPhase);
|
||||
|
||||
private final NumberAxis mEqualizerHistoryAxis = new NumberAxis();
|
||||
private final NumberAxis mEqualizerValueAxis = new NumberAxis(-1.5, 1.5, .1);
|
||||
private final LineChart<Number, Number> mEqualizerChart = new LineChart<>(mEqualizerHistoryAxis, mEqualizerValueAxis);
|
||||
private int mEqualizerPointer = 0;
|
||||
private int mEqualizerHistorySize = 40;
|
||||
private float[] mEqualizerBalanceHistory = new float[mEqualizerHistorySize];
|
||||
private float[] mEqualizerGainHistory = new float[mEqualizerHistorySize];
|
||||
|
||||
private final Button mReleaseButton = new Button("Next Sync");
|
||||
private final Label mPatternLabel = new Label();
|
||||
private CountDownLatch mRelease;
|
||||
private int mSymbolPointer = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public SyncVisualizer()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
mConstellationI.setLabel("Constellation - 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);
|
||||
mConstellationChart.setAnimated(false);
|
||||
mSampleChart.setAnimated(false);
|
||||
mSymbolChart.setAnimated(false);
|
||||
mSymbolChart.setId("symbol-chart");
|
||||
mEqualizerChart.setAnimated(false);
|
||||
|
||||
mEqualizerValueAxis.setLabel("Value");
|
||||
mEqualizerHistoryAxis.setLabel("Equalizer");
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.getChildren().add(mConstellationChart);
|
||||
HBox.setHgrow(mEqualizerChart, Priority.ALWAYS);
|
||||
hbox.getChildren().add(mEqualizerChart);
|
||||
// VBox.setVgrow(hbox, Priority.ALWAYS);
|
||||
|
||||
VBox.setVgrow(mSampleChart, Priority.ALWAYS);
|
||||
VBox.setVgrow(mSymbolChart, Priority.ALWAYS);
|
||||
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.BASELINE_LEFT);
|
||||
buttons.setSpacing(10);
|
||||
buttons.getChildren().addAll(mReleaseButton, mPatternLabel);
|
||||
VBox.setVgrow(buttons, Priority.NEVER);
|
||||
|
||||
//Load the symbol chart with symbols
|
||||
mSymbolSeries.setName("Symbols");
|
||||
for(int x = 0; x < SYMBOL_CHART_SERIES_COUNT; x++)
|
||||
{
|
||||
mSymbolSeries.getData().add(new XYChart.Data<>(x, 0));
|
||||
}
|
||||
mSymbolChart.getData().add(mSymbolSeries);
|
||||
float pi_4 = (float)Math.PI / 4f;
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 3f * pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 0));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -3 * pi_4));
|
||||
mSymbolChart.getData().add(mSymbolSeriesMarker);
|
||||
getChildren().addAll(buttons, hbox, mSampleChart, mSymbolChart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void symbol(float symbol)
|
||||
{
|
||||
for(XYChart.Data<Number, Number> data : mSymbolSeriesMarker.getData())
|
||||
{
|
||||
data.setXValue(mSymbolPointer);
|
||||
}
|
||||
mSymbolSeries.getData().get(mSymbolPointer++).setYValue(symbol);
|
||||
mSymbolPointer %= SYMBOL_CHART_SERIES_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, float equalizerBalance, float equalizerGain, String label, CountDownLatch latch)
|
||||
{
|
||||
mRelease = latch;
|
||||
mReleaseButton.setDisable(latch == null);
|
||||
|
||||
mEqualizerBalanceHistory[mEqualizerPointer] = equalizerBalance;
|
||||
mEqualizerGainHistory[mEqualizerPointer++] = (equalizerGain - 1.0f);
|
||||
mEqualizerPointer %= mEqualizerHistorySize;
|
||||
//Clear the next/upcoming values to give visual indication of where we're at.
|
||||
mEqualizerBalanceHistory[mEqualizerPointer] = 0f;
|
||||
mEqualizerGainHistory[mEqualizerPointer] = 0f;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
XYChart.Series<Number, Number> constellationIdeal = new XYChart.Series<>();
|
||||
constellationIdeal.setName("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("Decoded");
|
||||
|
||||
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);
|
||||
mEqualizerChart.getData().clear();
|
||||
|
||||
XYChart.Series<Number, Number> balanceSeries = new XYChart.Series<>();
|
||||
balanceSeries.setName("Balance");
|
||||
XYChart.Series<Number, Number> gainSeries = new XYChart.Series<>();
|
||||
gainSeries.setName("Gain (Value minus 1.0)");
|
||||
|
||||
for(int x = 0; x < mEqualizerHistorySize; x++)
|
||||
{
|
||||
balanceSeries.getData().add(new XYChart.Data<>(x, mEqualizerBalanceHistory[x]));
|
||||
gainSeries.getData().add(new XYChart.Data<>(x, mEqualizerGainHistory[x]));
|
||||
}
|
||||
|
||||
mEqualizerChart.getData().add(balanceSeries);
|
||||
mEqualizerChart.getData().add(gainSeries);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user