mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
330 WIP - updates the demodulator viewer with an eye diagram and symbol
chart. Testing various configurations to see which yields the best PLL and symbol timing locks.
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
package io.github.dsheirer.buffer;
|
||||
|
||||
import org.apache.commons.math3.stat.descriptive.moment.Mean;
|
||||
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
|
||||
import org.apache.commons.math3.stat.descriptive.moment.Variance;
|
||||
import org.apache.commons.math3.stat.descriptive.rank.Max;
|
||||
import org.apache.commons.math3.stat.descriptive.rank.Min;
|
||||
@@ -30,6 +31,7 @@ import java.util.Arrays;
|
||||
*/
|
||||
public class DoubleCircularBuffer
|
||||
{
|
||||
private StandardDeviation mStandardDeviation;
|
||||
private Variance mVariance;
|
||||
private Max mMax;
|
||||
private Mean mMean;
|
||||
@@ -140,6 +142,16 @@ public class DoubleCircularBuffer
|
||||
return mMin.evaluate(mBuffer, 0, mBuffer.length);
|
||||
}
|
||||
|
||||
public double standardDeviation()
|
||||
{
|
||||
if(mStandardDeviation == null)
|
||||
{
|
||||
mStandardDeviation = new StandardDeviation();
|
||||
}
|
||||
|
||||
return mStandardDeviation.evaluate(mBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the variance of the values contained in this buffer
|
||||
*/
|
||||
|
||||
@@ -12,155 +12,169 @@ import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class FilterView extends BorderPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( FilterView.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FilterView.class);
|
||||
|
||||
private CurveFittedAreaChart mImpulseChart;
|
||||
private NumberAxis mImpulseXAxis;
|
||||
private NumberAxis mImpulseYAxis;
|
||||
private CurveFittedAreaChart mDFTChart;
|
||||
private NumberAxis mDFTXAxis;
|
||||
private NumberAxis mDFTYAxis;
|
||||
private static final int FFT_SIZE = 4096;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE );
|
||||
|
||||
public FilterView( float[] taps )
|
||||
{
|
||||
init();
|
||||
private CurveFittedAreaChart mImpulseChart;
|
||||
private NumberAxis mImpulseXAxis;
|
||||
private NumberAxis mImpulseYAxis;
|
||||
private CurveFittedAreaChart mDFTChart;
|
||||
private NumberAxis mDFTXAxis;
|
||||
private NumberAxis mDFTYAxis;
|
||||
private static final int FFT_SIZE = 4096;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE);
|
||||
|
||||
if( taps != null )
|
||||
{
|
||||
setImpulseTaps( taps );
|
||||
public FilterView(float[] taps)
|
||||
{
|
||||
init();
|
||||
|
||||
float[] dft = new float[ FFT_SIZE ];
|
||||
System.arraycopy( taps, 0, dft, 0, taps.length );
|
||||
|
||||
mFFT.realForward( dft );
|
||||
|
||||
setDFTTaps( convertDFTToDecibel( dft, taps.length ) );
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
|
||||
mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 );
|
||||
mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 );
|
||||
|
||||
mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis );
|
||||
mImpulseChart.setLegendVisible( false );
|
||||
mImpulseChart.setHorizontalGridLinesVisible( false );
|
||||
mImpulseChart.setVerticalGridLinesVisible(false);
|
||||
if(taps != null)
|
||||
{
|
||||
setImpulseTaps(taps);
|
||||
|
||||
float[] dft = new float[FFT_SIZE];
|
||||
System.arraycopy(taps, 0, dft, 0, taps.length);
|
||||
|
||||
mFFT.realForward(dft);
|
||||
|
||||
setDFTTaps(convertDFTToDecibel(dft, taps.length));
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
|
||||
mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0);
|
||||
mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1);
|
||||
|
||||
mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis);
|
||||
mImpulseChart.setLegendVisible(false);
|
||||
mImpulseChart.setHorizontalGridLinesVisible(false);
|
||||
mImpulseChart.setVerticalGridLinesVisible(false);
|
||||
mImpulseChart.setAlternativeColumnFillVisible(false);
|
||||
mImpulseChart.setAlternativeRowFillVisible(false);
|
||||
|
||||
String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm();
|
||||
mImpulseChart.getStylesheets().add( css );
|
||||
|
||||
hbox.getChildren().add( mImpulseChart );
|
||||
HBox.setHgrow( mImpulseChart, Priority.ALWAYS );
|
||||
|
||||
mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 );
|
||||
mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 );
|
||||
|
||||
mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis );
|
||||
mDFTChart.setLegendVisible( false );
|
||||
mDFTChart.setHorizontalGridLinesVisible( false );
|
||||
mDFTChart.setVerticalGridLinesVisible(false);
|
||||
URL cssURL = FilterView.class.getResource("FilterView.css");
|
||||
|
||||
String css = null;
|
||||
|
||||
if(cssURL != null)
|
||||
{
|
||||
css = cssURL.toExternalForm();
|
||||
|
||||
}
|
||||
|
||||
hbox.getChildren().add(mImpulseChart);
|
||||
HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
|
||||
|
||||
mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8);
|
||||
mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0);
|
||||
|
||||
mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis);
|
||||
mDFTChart.setLegendVisible(false);
|
||||
mDFTChart.setHorizontalGridLinesVisible(false);
|
||||
mDFTChart.setVerticalGridLinesVisible(false);
|
||||
mDFTChart.setAlternativeColumnFillVisible(false);
|
||||
mDFTChart.setAlternativeRowFillVisible(false);
|
||||
mDFTChart.getStylesheets().add( css );
|
||||
|
||||
hbox.getChildren().add( mDFTChart );
|
||||
HBox.setHgrow( mDFTChart, Priority.ALWAYS );
|
||||
|
||||
setCenter( hbox );
|
||||
}
|
||||
|
||||
public void setImpulseTaps( double[] taps )
|
||||
{
|
||||
float[] convertedTaps = new float[ taps.length ];
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
{
|
||||
convertedTaps[ x ] = (float)taps[ x ];
|
||||
}
|
||||
|
||||
setImpulseTaps( convertedTaps );
|
||||
}
|
||||
|
||||
public void setImpulseTaps( float[] taps )
|
||||
{
|
||||
double smallest = 0.0;
|
||||
double largest = 0.0;
|
||||
|
||||
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
if(css != null)
|
||||
{
|
||||
if( taps[ x ] < smallest )
|
||||
{
|
||||
smallest = taps[ x ];
|
||||
}
|
||||
|
||||
if( taps[ x ] > largest )
|
||||
{
|
||||
largest = taps[ x ];
|
||||
}
|
||||
|
||||
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
|
||||
mImpulseChart.getStylesheets().add(css);
|
||||
mDFTChart.getStylesheets().add(css);
|
||||
}
|
||||
|
||||
mImpulseXAxis.setLowerBound( 0.0 );
|
||||
mImpulseXAxis.setUpperBound( taps.length - 1 );
|
||||
mImpulseXAxis.setTickUnit( 1.0 );
|
||||
|
||||
double padding = Math.abs( largest - smallest ) * .1;
|
||||
|
||||
mImpulseYAxis.setLowerBound( smallest - padding );
|
||||
mImpulseYAxis.setUpperBound( largest + padding );
|
||||
mImpulseYAxis.setTickUnit( padding );
|
||||
|
||||
hbox.getChildren().add(mDFTChart);
|
||||
HBox.setHgrow(mDFTChart, Priority.ALWAYS);
|
||||
|
||||
setCenter(hbox);
|
||||
}
|
||||
|
||||
public void setImpulseTaps(double[] taps)
|
||||
{
|
||||
float[] convertedTaps = new float[taps.length];
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
convertedTaps[x] = (float)taps[x];
|
||||
}
|
||||
|
||||
setImpulseTaps(convertedTaps);
|
||||
}
|
||||
|
||||
public void setImpulseTaps(float[] taps)
|
||||
{
|
||||
double smallest = 0.0;
|
||||
double largest = 0.0;
|
||||
|
||||
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
if(taps[x] < smallest)
|
||||
{
|
||||
smallest = taps[x];
|
||||
}
|
||||
|
||||
if(taps[x] > largest)
|
||||
{
|
||||
largest = taps[x];
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
|
||||
}
|
||||
|
||||
mImpulseXAxis.setLowerBound(0.0);
|
||||
mImpulseXAxis.setUpperBound(taps.length - 1);
|
||||
mImpulseXAxis.setTickUnit(1.0);
|
||||
|
||||
double padding = Math.abs(largest - smallest) * .1;
|
||||
|
||||
mImpulseYAxis.setLowerBound(smallest - padding);
|
||||
mImpulseYAxis.setUpperBound(largest + padding);
|
||||
mImpulseYAxis.setTickUnit(padding);
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mImpulseChart.getData();
|
||||
list.add( series );
|
||||
}
|
||||
|
||||
public void setDFTTaps( float[] taps )
|
||||
{
|
||||
double smallest = 0.0;
|
||||
|
||||
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
{
|
||||
if( taps[ x ] < smallest )
|
||||
{
|
||||
smallest = taps[ x ];
|
||||
}
|
||||
|
||||
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
|
||||
}
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mDFTChart.getData();
|
||||
list.add( series );
|
||||
}
|
||||
|
||||
public float[] convertDFTToDecibel( float[] dft, int tapLength )
|
||||
list.add(series);
|
||||
}
|
||||
|
||||
public void setDFTTaps(float[] taps)
|
||||
{
|
||||
float[] decibels = new float[ dft.length / 2 ];
|
||||
double smallest = 0.0;
|
||||
|
||||
int index = 0;
|
||||
|
||||
for( int x = 0; x < decibels.length; x ++ )
|
||||
{
|
||||
index = x * 2;
|
||||
|
||||
decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) +
|
||||
( dft[ index + 1 ] * dft[ index + 1 ] ) ) );
|
||||
}
|
||||
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
|
||||
|
||||
return decibels;
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
if(taps[x] < smallest)
|
||||
{
|
||||
smallest = taps[x];
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
|
||||
}
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mDFTChart.getData();
|
||||
list.add(series);
|
||||
}
|
||||
|
||||
public float[] convertDFTToDecibel(float[] dft, int tapLength)
|
||||
{
|
||||
float[] decibels = new float[dft.length / 2];
|
||||
|
||||
int index = 0;
|
||||
|
||||
for(int x = 0; x < decibels.length; x++)
|
||||
{
|
||||
index = x * 2;
|
||||
|
||||
decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) +
|
||||
(dft[index + 1] * dft[index + 1])));
|
||||
}
|
||||
|
||||
return decibels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ public class FilterViewer extends Application
|
||||
// .build();
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate((int)48000.0)
|
||||
.passBandCutoff(6000)
|
||||
.passBandCutoff(2880)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.1)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandStart(6250)
|
||||
.stopBandStart(3600)
|
||||
.stopBandRipple(0.01)
|
||||
.build();
|
||||
|
||||
@@ -81,6 +81,9 @@ public class FilterViewer extends Application
|
||||
|
||||
float[] taps = null;
|
||||
|
||||
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
|
||||
|
||||
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
|
||||
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
|
||||
// Window.WindowType.HAMMING, true);
|
||||
|
||||
|
||||
@@ -1,78 +1,103 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014,2015 Dennis Sheirer
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public abstract class FMDemodulator
|
||||
{
|
||||
private float mPreviousI = 0.0f;
|
||||
private float mPreviousQ = 0.0f;
|
||||
protected float mGain;
|
||||
private float mPreviousI = 0.0f;
|
||||
private float mPreviousQ = 0.0f;
|
||||
protected float mGain;
|
||||
|
||||
public FMDemodulator( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
public FMDemodulator(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public float demodulate( float currentI, float currentQ )
|
||||
{
|
||||
/**
|
||||
* Multiply the current sample against the complex conjugate of the
|
||||
* previous sample to derive the phase delta between the two samples
|
||||
*
|
||||
* Negating the previous sample quadrature produces the conjugate
|
||||
*/
|
||||
double inphase = ( currentI * mPreviousI ) - ( currentQ * -mPreviousQ );
|
||||
double quadrature = ( currentQ * mPreviousI ) + ( currentI * -mPreviousQ );
|
||||
|
||||
double angle = 0.0f;
|
||||
|
||||
//Check for divide by zero
|
||||
if( inphase != 0 )
|
||||
{
|
||||
/**
|
||||
* Use the arc-tangent of quadrature divided by inphase to
|
||||
* get the phase angle (+/-) which was directly manipulated by the
|
||||
* original message waveform during the modulation. This value now
|
||||
* serves as the instantaneous amplitude of the demodulated signal
|
||||
*/
|
||||
double denominator = 1.0d / inphase;
|
||||
angle = Math.atan( (double)quadrature * denominator );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the current sample to use during the next iteration
|
||||
*/
|
||||
mPreviousI = currentI;
|
||||
mPreviousQ = currentQ;
|
||||
|
||||
return (float)( angle * mGain );
|
||||
}
|
||||
public float demodulate(float currentI, float currentQ)
|
||||
{
|
||||
/**
|
||||
* Multiply the current sample against the complex conjugate of the
|
||||
* previous sample to derive the phase delta between the two samples
|
||||
*
|
||||
* Negating the previous sample quadrature produces the conjugate
|
||||
*/
|
||||
double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
|
||||
double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
|
||||
|
||||
public abstract void dispose();
|
||||
double angle = 0.0f;
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mPreviousI = 0.0f;
|
||||
mPreviousQ = 0.0f;
|
||||
}
|
||||
//Check for divide by zero
|
||||
if(inphase != 0)
|
||||
{
|
||||
/**
|
||||
* Use the arc-tangent of quadrature divided by inphase to
|
||||
* get the phase angle (+/-) which was directly manipulated by the
|
||||
* original message waveform during the modulation. This value now
|
||||
* serves as the instantaneous amplitude of the demodulated signal
|
||||
*/
|
||||
double denominator = 1.0d / inphase;
|
||||
angle = Math.atan((double)quadrature * denominator);
|
||||
}
|
||||
|
||||
public void setGain( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
/**
|
||||
* Store the current sample to use during the next iteration
|
||||
*/
|
||||
mPreviousI = currentI;
|
||||
mPreviousQ = currentQ;
|
||||
|
||||
return (float)(angle * mGain);
|
||||
}
|
||||
|
||||
public static double demodulate(Complex previous, Complex current)
|
||||
{
|
||||
double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature());
|
||||
double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature());
|
||||
|
||||
double angle = 0.0f;
|
||||
|
||||
//Check for divide by zero
|
||||
if(inphase != 0)
|
||||
{
|
||||
/**
|
||||
* Use the arc-tangent of quadrature divided by inphase to
|
||||
* get the phase angle (+/-) which was directly manipulated by the
|
||||
* original message waveform during the modulation. This value now
|
||||
* serves as the instantaneous amplitude of the demodulated signal
|
||||
*/
|
||||
double denominator = 1.0d / inphase;
|
||||
angle = Math.atan((double)quadrature * denominator);
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
public abstract void dispose();
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mPreviousI = 0.0f;
|
||||
mPreviousQ = 0.0f;
|
||||
}
|
||||
|
||||
public void setGain(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FMDemodulator_CB extends FMDemodulator implements Listener<ComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( FMDemodulator_CB.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class );
|
||||
|
||||
private Listener<RealBuffer> mListener;
|
||||
|
||||
|
||||
@@ -33,11 +33,13 @@ public class InterpolatingSymbolBuffer
|
||||
private float mOmegaMid;
|
||||
|
||||
private int mDelayLinePointer = 0;
|
||||
private float mSamplesPerSymbol;
|
||||
private int mTwiceSamplesPerSymbol;
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
|
||||
public InterpolatingSymbolBuffer(float samplesPerSymbol)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mMu = samplesPerSymbol;
|
||||
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
|
||||
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
|
||||
@@ -58,7 +60,8 @@ public class InterpolatingSymbolBuffer
|
||||
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature();
|
||||
|
||||
//Increment pointer and keep pointer in bounds
|
||||
mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol;
|
||||
mDelayLinePointer++;
|
||||
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,11 +83,11 @@ public class InterpolatingSymbolBuffer
|
||||
{
|
||||
//mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
|
||||
//symbol
|
||||
mOmega = mOmega + mGainOmega * symbolTimingError;
|
||||
mOmega = mOmega + mGainOmega * -symbolTimingError;
|
||||
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||
|
||||
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
|
||||
increaseSampleCounter((mOmega + (mGainMu * symbolTimingError)));
|
||||
increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +131,18 @@ public class InterpolatingSymbolBuffer
|
||||
return mCurrentSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
|
||||
* be used to support an external eye-diagram chart.
|
||||
* @return symbol decision data.
|
||||
*/
|
||||
public SymbolDecisionData getSymbolDecisionData()
|
||||
{
|
||||
//TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long
|
||||
return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature,
|
||||
mSamplesPerSymbol, mDelayLinePointer, mMu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolated inphase value for the specified offset
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -31,7 +32,9 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private GardnerDetector mGardnerDetector = new GardnerDetector();
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Dibit> mDibitListener;
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
|
||||
private Complex mPreviousSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
@@ -42,6 +45,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private Complex mCurrentSymbol = new Complex(0, 0);
|
||||
private float mPhaseError;
|
||||
private float mSymbolTimingError;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
|
||||
@@ -63,6 +67,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
"] must be at least 2 x symbol rate [" + symbolRate + "]");
|
||||
}
|
||||
|
||||
mSampleRate = sampleRate;
|
||||
mPLL = phaseLockedLoop;
|
||||
mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator;
|
||||
mQPSKSymbolDecoder = symbolDecoder;
|
||||
@@ -108,6 +113,12 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample();
|
||||
mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample();
|
||||
|
||||
//Eye diagram listener
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData());
|
||||
}
|
||||
|
||||
//Calculate middle and current symbols as the delta between the previous and current samples
|
||||
mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
@@ -127,12 +138,20 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPreviousMiddleSample.setValues(mMiddleSample);
|
||||
mPreviousSymbol.setValues(mCurrentSymbol);
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//Adjust the current symbol as necessary (e.g. for differential encoding)
|
||||
mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
|
||||
|
||||
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
||||
//feedback to the PLL
|
||||
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
|
||||
|
||||
mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f);
|
||||
mPLL.adjust(mPhaseError);
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
@@ -140,18 +159,21 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPLLErrorListener.receive((double)mPhaseError);
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * Math.PI);
|
||||
|
||||
mPLLFrequencyListener.receive(loopFrequency);
|
||||
}
|
||||
|
||||
//Decode the dibit from the symbol and send to the listener
|
||||
if(mDibitListener != null)
|
||||
{
|
||||
mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol));
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//TODO: Assemble dibits here and broadcast
|
||||
}
|
||||
}
|
||||
@@ -179,4 +201,18 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
|
||||
public class SymbolDecisionData
|
||||
{
|
||||
private float[] mSamplesInphase;
|
||||
private float[] mSamplesQuadrature;
|
||||
private float mSamplesPerSymbol;
|
||||
private int mSampleIndex;
|
||||
private float mSampleIndexOffset;
|
||||
|
||||
/**
|
||||
* Symbol decision data - samples and timing decision for a single symbol decision instant.
|
||||
*
|
||||
* @param samplesInphase
|
||||
* @param samplesQuadrature
|
||||
* @param samplesPerSymbol
|
||||
* @param sampleIndex
|
||||
* @param sampleIndexOffset
|
||||
*/
|
||||
public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol,
|
||||
int sampleIndex, float sampleIndexOffset)
|
||||
{
|
||||
mSamplesInphase = samplesInphase;
|
||||
mSamplesQuadrature = samplesQuadrature;
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mSampleIndex = sampleIndex;
|
||||
mSampleIndexOffset = sampleIndexOffset;
|
||||
}
|
||||
|
||||
public float[] getInphaseSamples()
|
||||
{
|
||||
return mSamplesInphase;
|
||||
}
|
||||
|
||||
public float[] getQuadratureSamples()
|
||||
{
|
||||
return mSamplesQuadrature;
|
||||
}
|
||||
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return mSamplesPerSymbol;
|
||||
}
|
||||
|
||||
public int getSampleIndex()
|
||||
{
|
||||
return mSampleIndex;
|
||||
}
|
||||
|
||||
public float getSampleIndexOffset()
|
||||
{
|
||||
return mSampleIndexOffset;
|
||||
}
|
||||
}
|
||||
@@ -194,20 +194,21 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
|
||||
mErrorBuffer.put(phaseError);
|
||||
|
||||
double variance = mErrorBuffer.variance();
|
||||
double standardDeviation = mErrorBuffer.standardDeviation();
|
||||
|
||||
switch(mTracking)
|
||||
{
|
||||
case SEARCHING:
|
||||
if(Tracking.COARSE.meetsThreshold(variance))
|
||||
if(Tracking.COARSE.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.COARSE);
|
||||
}
|
||||
break;
|
||||
case COARSE:
|
||||
if(Tracking.COARSE.meetsThreshold(variance))
|
||||
if(Tracking.COARSE.contains(standardDeviation))
|
||||
{
|
||||
if(Tracking.FINE.meetsThreshold(variance))
|
||||
//Promote if possible - otherwise remain
|
||||
if(Tracking.FINE.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.FINE);
|
||||
}
|
||||
@@ -218,9 +219,10 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
break;
|
||||
case FINE:
|
||||
if(Tracking.FINE.meetsThreshold(variance))
|
||||
if(Tracking.FINE.contains(standardDeviation))
|
||||
{
|
||||
if(Tracking.LOCKED.meetsThreshold(variance))
|
||||
//Promote if possible - otherwise remain
|
||||
if(Tracking.LOCKED.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.LOCKED);
|
||||
}
|
||||
@@ -231,7 +233,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
break;
|
||||
case LOCKED:
|
||||
if(!Tracking.LOCKED.meetsThreshold(variance))
|
||||
if(!Tracking.LOCKED.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.FINE);
|
||||
}
|
||||
|
||||
@@ -17,32 +17,55 @@ package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
public enum Tracking
|
||||
{
|
||||
SEARCHING(50.0, Math.PI),
|
||||
COARSE(100.0, 0.01),
|
||||
FINE(150.0, 0.001),
|
||||
LOCKED(200.0, 0.0001);
|
||||
// SEARCHING(50.0, 0.01, Math.PI),
|
||||
// COARSE(100.0, 0.001, 0.01),
|
||||
// FINE(150.0, 0.0001, 0.001),
|
||||
// LOCKED(200.0, 0.0, 0.0001);
|
||||
SEARCHING(100.0, 0.4, Math.PI),
|
||||
COARSE(150.0, 0.2, 0.4),
|
||||
FINE(175.0, 0.1, 0.2),
|
||||
LOCKED(200.0, 0.0, 0.1);
|
||||
|
||||
private double mLoopBandwidth;
|
||||
private double mVarianceThreshold;
|
||||
private double mThresholdMinimum;
|
||||
private double mThresholdMaximum;
|
||||
|
||||
Tracking(double loopBandwidth, double varianceThreshold)
|
||||
Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum)
|
||||
{
|
||||
mLoopBandwidth = loopBandwidth;
|
||||
mVarianceThreshold = varianceThreshold;
|
||||
mThresholdMinimum = thresholdMinimum;
|
||||
mThresholdMaximum = thresholdMaximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop bandwidth setting
|
||||
*/
|
||||
public double getLoopBandwidth()
|
||||
{
|
||||
return mLoopBandwidth;
|
||||
}
|
||||
|
||||
public double getVarianceThreshold()
|
||||
/**
|
||||
* Minimum error value for this entry
|
||||
*/
|
||||
public double getThresholdMinimum()
|
||||
{
|
||||
return mVarianceThreshold;
|
||||
return mThresholdMinimum;
|
||||
}
|
||||
|
||||
public boolean meetsThreshold(double variance)
|
||||
/**
|
||||
* Maximum error value for this entry
|
||||
*/
|
||||
public double getThresholdMaximum()
|
||||
{
|
||||
return variance < getVarianceThreshold();
|
||||
return mThresholdMaximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the error value is within the range for this entry
|
||||
*/
|
||||
public boolean contains(double standardDeviation)
|
||||
{
|
||||
return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
|
||||
|
||||
public void load(File file)
|
||||
{
|
||||
mLog.debug("loading:" + file.getAbsolutePath());
|
||||
if(file != null && file.isFile())
|
||||
{
|
||||
try
|
||||
@@ -104,7 +103,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]");
|
||||
mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe);
|
||||
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file",
|
||||
ButtonType.OK);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.instrument.gui.viewer.chart;
|
||||
|
||||
import io.github.dsheirer.dsp.fm.FMDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisionData>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class);
|
||||
|
||||
private ObservableList<Series<Double,Double>> mData = FXCollections.observableArrayList();
|
||||
private int mSeriesCount;
|
||||
private int mSeriesPointer;
|
||||
private int mSeriesLength;
|
||||
|
||||
public EyeDiagramChart(int seriesCount)
|
||||
{
|
||||
super(new NumberAxis("Symbol Timing", 1.0, 10.0, 1.0),
|
||||
new NumberAxis("Value", -1.0, 1.0, 0.25));
|
||||
|
||||
mSeriesCount = seriesCount;
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
for(int x = 0; x < mSeriesCount; x++)
|
||||
{
|
||||
ObservableList<Data<Double,Double>> series = FXCollections.observableArrayList();
|
||||
mData.add(new Series<>(series));
|
||||
}
|
||||
|
||||
setData(mData);
|
||||
}
|
||||
|
||||
private ObservableList<Data<Double,Double>> getSeries(int series, int length)
|
||||
{
|
||||
ObservableList<Data<Double,Double>> seriesData = mData.get(series).getData();
|
||||
|
||||
while(seriesData.size() > length)
|
||||
{
|
||||
seriesData.remove(0);
|
||||
}
|
||||
|
||||
while(seriesData.size() < length)
|
||||
{
|
||||
seriesData.add(new Data<Double,Double>(0.0,0.0));
|
||||
}
|
||||
|
||||
return seriesData;
|
||||
}
|
||||
|
||||
private void checkChartLength(int length)
|
||||
{
|
||||
if(mSeriesLength != length)
|
||||
{
|
||||
((NumberAxis)getXAxis()).setUpperBound(length);
|
||||
mSeriesLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(SymbolDecisionData symbolDecisionData)
|
||||
{
|
||||
checkChartLength((int)symbolDecisionData.getSamplesPerSymbol());
|
||||
int length = (int)symbolDecisionData.getSamplesPerSymbol() + 1;
|
||||
|
||||
|
||||
ObservableList<Data<Double,Double>> series = getSeries(mSeriesPointer++, length + 1);
|
||||
|
||||
if(mSeriesPointer >= mSeriesCount)
|
||||
{
|
||||
mSeriesPointer = 0;
|
||||
}
|
||||
|
||||
int index = symbolDecisionData.getSampleIndex();
|
||||
|
||||
Complex previous = new Complex(symbolDecisionData.getInphaseSamples()[index],
|
||||
symbolDecisionData.getQuadratureSamples()[index]);
|
||||
|
||||
for(int x = 1; x <= length; x++)
|
||||
{
|
||||
Complex current = new Complex(symbolDecisionData.getInphaseSamples()[index + x],
|
||||
symbolDecisionData.getQuadratureSamples()[index + x]);
|
||||
|
||||
double demodulated = FMDemodulator.demodulate(previous, current);
|
||||
demodulated *= 6;
|
||||
|
||||
Data<Double,Double> dataPoint = series.get(x);
|
||||
dataPoint.setXValue((double)x - symbolDecisionData.getSampleIndexOffset());
|
||||
dataPoint.setYValue(demodulated);
|
||||
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.instrument.gui.viewer.chart;
|
||||
|
||||
import io.github.dsheirer.buffer.DoubleCircularBuffer;
|
||||
import io.github.dsheirer.dsp.fm.FMDemodulator;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FMDemodulatedLineChart extends LineChart implements Listener<ComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class);
|
||||
|
||||
private DoubleCircularBuffer mDoubleCircularBuffer;
|
||||
private int mSamplesPerSymbol;
|
||||
private int mTwiceSamplesPerSymbol;
|
||||
private int mSeriesCount;
|
||||
private ObservableList<Series<Integer,Double>> mData = FXCollections.observableArrayList();
|
||||
private Complex mPreviousSample = new Complex(0,0);
|
||||
private int mSeriesPointer;
|
||||
private int mSamplePointer;
|
||||
|
||||
public FMDemodulatedLineChart(int samplesPerSymbol, int seriesCount)
|
||||
{
|
||||
super(new NumberAxis("Time", 1, samplesPerSymbol * 2, 2),
|
||||
new NumberAxis("Demodulated", -1.0, 1.0, 0.25));
|
||||
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mTwiceSamplesPerSymbol = mSamplesPerSymbol * 2;
|
||||
mSeriesCount = seriesCount;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
int seriesLength = 2 * mSamplesPerSymbol;
|
||||
mDoubleCircularBuffer = new DoubleCircularBuffer(seriesLength * mSeriesCount);
|
||||
|
||||
for(int x = 0; x < mSeriesCount; x++)
|
||||
{
|
||||
ObservableList<Data<Integer,Double>> series = FXCollections.observableArrayList();
|
||||
|
||||
for(int y = 0; y < seriesLength; y++)
|
||||
{
|
||||
series.add(new Data<Integer,Double>(y + 1, 0.0));
|
||||
}
|
||||
|
||||
mData.add(new Series<>(series));
|
||||
}
|
||||
|
||||
setData(mData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
Complex sample;
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
sample = new Complex(samples[x], samples[x + 1]);
|
||||
double demodulated = FMDemodulator.demodulate(mPreviousSample, sample);
|
||||
demodulated *= 2.0;
|
||||
|
||||
if(demodulated > 1.0)
|
||||
{
|
||||
demodulated = 1.0;
|
||||
}
|
||||
else if(demodulated < -1.0)
|
||||
{
|
||||
demodulated = -1.0;
|
||||
}
|
||||
mDoubleCircularBuffer.put(demodulated);
|
||||
mPreviousSample = sample;
|
||||
updateChart(demodulated);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChart(double value)
|
||||
{
|
||||
Series<Integer,Double> series = mData.get(mSeriesPointer);
|
||||
Data data = series.getData().get(mSamplePointer++);
|
||||
data.setYValue(value);
|
||||
|
||||
if(mSamplePointer >= mTwiceSamplesPerSymbol)
|
||||
{
|
||||
mSamplePointer = 0;
|
||||
mSeriesPointer++;
|
||||
|
||||
if(mSeriesPointer >= mSeriesCount)
|
||||
{
|
||||
mSeriesPointer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.instrument.gui.viewer.chart;
|
||||
|
||||
import io.github.dsheirer.buffer.DoubleCircularBuffer;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FrequencyLineChart extends LineChart implements Listener<Double>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class);
|
||||
|
||||
private DoubleCircularBuffer mDoubleCircularBuffer;
|
||||
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
|
||||
|
||||
public FrequencyLineChart(double maxFrequency, int length)
|
||||
{
|
||||
super(new NumberAxis("Time", 1, length, 2),
|
||||
new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0));
|
||||
|
||||
Series<Integer,Double> phaseSeries = new Series<>("PLL Frequency", mPhaseValues);
|
||||
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
|
||||
|
||||
setData(observableList);
|
||||
init(length);
|
||||
}
|
||||
|
||||
private void init(int length)
|
||||
{
|
||||
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
|
||||
|
||||
for(int x = 1; x <= length; x++)
|
||||
{
|
||||
mPhaseValues.add(new Data(x, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Double phase)
|
||||
{
|
||||
mDoubleCircularBuffer.put(phase);
|
||||
|
||||
updateChart();
|
||||
}
|
||||
|
||||
private void updateChart()
|
||||
{
|
||||
double[] values = mDoubleCircularBuffer.getAll();
|
||||
|
||||
for(int x = 0; x < values.length; x++)
|
||||
{
|
||||
mPhaseValues.get(x).setYValue(values[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,9 @@ public class PhaseLineChart extends LineChart implements Listener<Double>
|
||||
public PhaseLineChart(int length)
|
||||
{
|
||||
super(new NumberAxis("Time", 1, length, 2),
|
||||
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
|
||||
new NumberAxis("Phase", -0.8, 0.8, 0.1));
|
||||
|
||||
Series<Integer,Double> phaseSeries = new Series<>("Phase", mPhaseValues);
|
||||
Series<Integer,Double> phaseSeries = new Series<>("PLL Phase Error", mPhaseValues);
|
||||
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
|
||||
|
||||
setData(observableList);
|
||||
|
||||
@@ -30,8 +30,8 @@ public class SymbolChart extends ScatterChart implements Listener<Complex>
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class);
|
||||
|
||||
private ObservableList<ScatterChart.Data> mCurrentPoints = FXCollections.observableArrayList();
|
||||
private ObservableList<ScatterChart.Data> mPreviousPoints = FXCollections.observableArrayList();
|
||||
private ObservableList<ScatterChart.Data> mOldestPoints = FXCollections.observableArrayList();
|
||||
// private ObservableList<ScatterChart.Data> mPreviousPoints = FXCollections.observableArrayList();
|
||||
// private ObservableList<ScatterChart.Data> mOldestPoints = FXCollections.observableArrayList();
|
||||
private int mDataPointer;
|
||||
private int mHistory;
|
||||
|
||||
@@ -43,22 +43,22 @@ public class SymbolChart extends ScatterChart implements Listener<Complex>
|
||||
*/
|
||||
public SymbolChart(int history)
|
||||
{
|
||||
super(new NumberAxis("I", -1.0, 1.0, 0.2),
|
||||
new NumberAxis("Q", -1.0, 1.0, 0.2));
|
||||
super(new NumberAxis("I", -1.1, 1.1, 0.2),
|
||||
new NumberAxis("Q", -1.1, 1.1, 0.2));
|
||||
|
||||
mHistory = history;
|
||||
|
||||
for(int x = 0; x < mHistory; x++)
|
||||
{
|
||||
mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0));
|
||||
mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0));
|
||||
mOldestPoints.add(new XYChart.Data<>(0.0, 0.0));
|
||||
// mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0));
|
||||
// mOldestPoints.add(new XYChart.Data<>(0.0, 0.0));
|
||||
}
|
||||
|
||||
ObservableList<XYChart.Series> data = FXCollections.observableArrayList();
|
||||
data.add(new ScatterChart.Series("Current", mCurrentPoints));
|
||||
data.add(new ScatterChart.Series("Previous", mPreviousPoints));
|
||||
data.add(new ScatterChart.Series("Oldest", mOldestPoints));
|
||||
// data.add(new ScatterChart.Series("Previous", mPreviousPoints));
|
||||
// data.add(new ScatterChart.Series("Oldest", mOldestPoints));
|
||||
|
||||
this.setData(data);
|
||||
}
|
||||
@@ -67,15 +67,15 @@ public class SymbolChart extends ScatterChart implements Listener<Complex>
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
//Transfer previous to oldest
|
||||
XYChart.Data oldest = mOldestPoints.get(mDataPointer);
|
||||
XYChart.Data previous = mPreviousPoints.get(mDataPointer);
|
||||
// XYChart.Data oldest = mOldestPoints.get(mDataPointer);
|
||||
// XYChart.Data previous = mPreviousPoints.get(mDataPointer);
|
||||
XYChart.Data current = mCurrentPoints.get(mDataPointer);
|
||||
|
||||
oldest.setXValue(previous.getXValue());
|
||||
oldest.setYValue(previous.getYValue());
|
||||
|
||||
previous.setXValue(current.getXValue());
|
||||
previous.setYValue(current.getYValue());
|
||||
// oldest.setXValue(previous.getXValue());
|
||||
// oldest.setYValue(previous.getYValue());
|
||||
//
|
||||
// previous.setXValue(current.getXValue());
|
||||
// previous.setYValue(current.getYValue());
|
||||
|
||||
current.setXValue(complex.inphase());
|
||||
current.setYValue(complex.quadrature());
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.instrument.gui.viewer.decoder;
|
||||
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart;
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart;
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.FrequencyLineChart;
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
|
||||
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
@@ -32,10 +33,11 @@ public class P25Phase1Pane extends DecoderPane
|
||||
|
||||
private HBox mSampleChartBox;
|
||||
private ComplexSampleLineChart mSampleLineChart;
|
||||
private ComplexPhaseLineChart mPhaseLineChart;
|
||||
private EyeDiagramChart mEyeDiagramChart;
|
||||
private HBox mDecoderChartBox;
|
||||
private SymbolChart mSymbolChart;
|
||||
private PhaseLineChart mPLLPhaseErrorLineChart;
|
||||
private FrequencyLineChart mPLLFrequencyLineChart;
|
||||
private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null);
|
||||
|
||||
public P25Phase1Pane()
|
||||
@@ -47,11 +49,12 @@ public class P25Phase1Pane extends DecoderPane
|
||||
private void init()
|
||||
{
|
||||
addListener(getSampleLineChart());
|
||||
addListener(getPhaseLineChart());
|
||||
addListener(getDecoder());
|
||||
|
||||
getDecoder().setSymbolListener(getSymbolChart());
|
||||
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
||||
getDecoder().setDEBUGSymbolListener(getSymbolChart());
|
||||
getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
||||
getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart());
|
||||
getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart());
|
||||
|
||||
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
|
||||
}
|
||||
@@ -73,7 +76,7 @@ public class P25Phase1Pane extends DecoderPane
|
||||
{
|
||||
if(mSymbolChart == null)
|
||||
{
|
||||
mSymbolChart = new SymbolChart(20);
|
||||
mSymbolChart = new SymbolChart(10);
|
||||
}
|
||||
|
||||
return mSymbolChart;
|
||||
@@ -86,9 +89,11 @@ public class P25Phase1Pane extends DecoderPane
|
||||
mDecoderChartBox = new HBox();
|
||||
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
|
||||
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart());
|
||||
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
|
||||
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart());
|
||||
}
|
||||
|
||||
return mDecoderChartBox;
|
||||
@@ -100,10 +105,10 @@ public class P25Phase1Pane extends DecoderPane
|
||||
{
|
||||
mSampleChartBox = new HBox();
|
||||
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPhaseLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS);
|
||||
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart());
|
||||
HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
|
||||
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart());
|
||||
}
|
||||
|
||||
return mSampleChartBox;
|
||||
@@ -119,23 +124,33 @@ public class P25Phase1Pane extends DecoderPane
|
||||
return mSampleLineChart;
|
||||
}
|
||||
|
||||
private ComplexPhaseLineChart getPhaseLineChart()
|
||||
private EyeDiagramChart getEyeDiagramChart()
|
||||
{
|
||||
if(mPhaseLineChart == null)
|
||||
if(mEyeDiagramChart == null)
|
||||
{
|
||||
mPhaseLineChart = new ComplexPhaseLineChart(50);
|
||||
mEyeDiagramChart = new EyeDiagramChart(10);
|
||||
}
|
||||
|
||||
return mPhaseLineChart;
|
||||
return mEyeDiagramChart;
|
||||
}
|
||||
|
||||
private PhaseLineChart getPLLPhaseErrorLineChart()
|
||||
{
|
||||
if(mPLLPhaseErrorLineChart == null)
|
||||
{
|
||||
mPLLPhaseErrorLineChart = new PhaseLineChart(80);
|
||||
mPLLPhaseErrorLineChart = new PhaseLineChart(40);
|
||||
}
|
||||
|
||||
return mPLLPhaseErrorLineChart;
|
||||
}
|
||||
|
||||
private FrequencyLineChart getPLLFrequencyLineChart()
|
||||
{
|
||||
if(mPLLFrequencyLineChart == null)
|
||||
{
|
||||
mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40);
|
||||
}
|
||||
|
||||
return mPLLFrequencyLineChart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +698,8 @@ public class P25MessageFramer implements Listener<Dibit>
|
||||
@Override
|
||||
public void syncDetected()
|
||||
{
|
||||
mPhaseLockedLoop.correctInversion(mCorrection);
|
||||
mLog.warn("Phase Inversion SYNC detected - applying correction: " + mCorrection);
|
||||
// mPhaseLockedLoop.correctInversion(mCorrection);
|
||||
|
||||
/* Since we detected a sync pattern, start a message assembler */
|
||||
for(P25MessageAssembler assembler : mAssemblers)
|
||||
|
||||
@@ -26,8 +26,9 @@ import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
|
||||
import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
|
||||
import io.github.dsheirer.dsp.psk.QPSKDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.Tracking;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
@@ -49,16 +50,21 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
|
||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||
private ComplexFIRFilter mBasebandFilter;
|
||||
private Map<Double,float[]> mSymbolFilters = new HashMap<>();
|
||||
private ComplexFIRFilter mSymbolFilter;
|
||||
|
||||
private QPSKDemodulator mQPSKDemodulator;
|
||||
private IPhaseLockedLoop mPhaseLockedLoop;
|
||||
private CostasLoop mCostasLoop;
|
||||
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
||||
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
||||
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
|
||||
private P25MessageFramer mMessageFramer;
|
||||
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
|
||||
private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder();
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Double> mPLLPhaseErrorListener;
|
||||
private Listener<Complex> mDEBUGSymbolListener;
|
||||
private Listener<Double> mDEBUGPLLPhaseErrorListener;
|
||||
private Listener<Double> mDEBUGPLLFrequencyListener;
|
||||
private Listener<SymbolDecisionData> mDEBUGSymbolDecisionDataListener;
|
||||
|
||||
public P25_C4FMDecoder2(AliasList aliasList)
|
||||
{
|
||||
@@ -78,37 +84,57 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
|
||||
mSampleRate = sampleRate;
|
||||
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
|
||||
mSymbolFilter = new ComplexFIRFilter(getSymbolFilter(), 1.0f);
|
||||
|
||||
mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
|
||||
mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
|
||||
mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
|
||||
mCostasLoop.setTracking(Tracking.FINE);
|
||||
mCostasLoop.setAutomaticTracking(false);
|
||||
|
||||
mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
|
||||
mSampleRate, SYMBOL_RATE);
|
||||
mQPSKDemodulator.setSymbolListener(mSymbolListener);
|
||||
mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener);
|
||||
mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener);
|
||||
mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener);
|
||||
mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener);
|
||||
mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener);
|
||||
|
||||
//Message framer can trigger a symbol-inversion correction to the PLL when detected
|
||||
mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop);
|
||||
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
|
||||
mMessageFramer.setListener(getMessageProcessor());
|
||||
mQPSKDemodulator.setDibitListener(mMessageFramer);
|
||||
}
|
||||
|
||||
public void setSymbolListener(Listener<Complex> listener)
|
||||
public void setDEBUGSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
mDEBUGSymbolListener = listener;
|
||||
mQPSKDemodulator.setSymbolListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLPhaseErrorListener(Listener<Double> listener)
|
||||
public void setDEBUGPLLPhaseErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLPhaseErrorListener = listener;
|
||||
mDEBUGPLLPhaseErrorListener = listener;
|
||||
mQPSKDemodulator.setPLLErrorListener(listener);
|
||||
}
|
||||
|
||||
public void setDEBUGPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mDEBUGPLLFrequencyListener = listener;
|
||||
mQPSKDemodulator.setPLLFrequencyListener(listener);
|
||||
}
|
||||
|
||||
public void setDEBUGSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mDEBUGSymbolDecisionDataListener = listener;
|
||||
mQPSKDemodulator.setSymbolDecisionDataListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer);
|
||||
ComplexBuffer amplified = mAGC.filter(filtered);
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer);
|
||||
// ComplexBuffer symbolFiltered = mBasebandFilter.filter(basebandFiltered);
|
||||
// ComplexBuffer amplified = mAGC.filter(symbolFiltered);
|
||||
mQPSKDemodulator.receive(basebandFiltered);
|
||||
// mQPSKDemodulator.receive(complexBuffer);
|
||||
}
|
||||
|
||||
private double getSampleRate()
|
||||
@@ -124,24 +150,44 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate((int)getSampleRate())
|
||||
.passBandCutoff(6000)
|
||||
.passBandCutoff(2880)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.1)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandStart(6250)
|
||||
.stopBandStart(3600)
|
||||
.stopBandRipple(0.01)
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
filter = FilterFactory.getTaps(specification);
|
||||
mBasebandFilters.put(getSampleRate(), filter);
|
||||
|
||||
}
|
||||
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 symbol filter for sample rate: " + mSampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private float[] getSymbolFilter()
|
||||
{
|
||||
float[] filter = mSymbolFilters.get(getSampleRate());
|
||||
|
||||
if(filter == null)
|
||||
{
|
||||
filter = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
|
||||
mSymbolFilters.put(getSampleRate(), filter);
|
||||
}
|
||||
|
||||
return filter;
|
||||
|
||||
@@ -167,13 +167,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mInputStream.close();
|
||||
mInputStream = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Can't close wave source - was not opened");
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user