Instrumentation updates for eye diagram data and removed stack trace logging when JMBE library is not discovered.

This commit is contained in:
dennis.sheirer
2015-06-19 09:34:36 +00:00
parent f9703368a0
commit 538a299246
11 changed files with 815 additions and 171 deletions

View File

@@ -48,7 +48,6 @@ import dsp.gain.DirectGainControl;
import dsp.nbfm.FMDiscriminator;
import dsp.psk.CQPSKDemodulator;
import dsp.psk.QPSKPolarSlicer;
import dsp.psk.QPSKStarSlicer;
public class P25Decoder extends Decoder implements Instrumentable
{
@@ -214,8 +213,12 @@ public class P25Decoder extends Decoder implements Instrumentable
if( mSourceSampleType == SampleType.COMPLEX )
{
mAvailableTaps.add( new ComplexTap( INSTRUMENT_COMPLEX_INPUT, 0, 1.0f ) );
mAvailableTaps.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
if( mCQPSKDemodulator != null )
{
mAvailableTaps.addAll( mCQPSKDemodulator.getTaps() );
}
// mAvailableTaps.add( new ComplexTap( INSTRUMENT_COMPLEX_INPUT, 0, 1.0f ) );
// mAvailableTaps.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
}
else
{
@@ -238,7 +241,6 @@ public class P25Decoder extends Decoder implements Instrumentable
mAvailableTaps.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
mAvailableTaps.add( new QPSKTap( INSTRUMENT_QPSK_DEMODULATOR_OUTPUT, 0, 1.0f ) );
mAvailableTaps.add( new DibitTap( INSTRUMENT_CQPSK_SLICER_OUTPUT, 0, 0.1f ) );
mAvailableTaps.addAll( mCQPSKDemodulator.getTaps() );
}
}
@@ -266,7 +268,7 @@ public class P25Decoder extends Decoder implements Instrumentable
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
ComplexTap baseband = (ComplexTap)tap;
mBasebandFilter.setListener( baseband );
baseband.setListener( mEqualizer );
baseband.setListener( mAGC );
break;
case INSTRUMENT_AGC_OUTPUT:
ComplexTap agcSymbol = (ComplexTap)tap;
@@ -323,7 +325,7 @@ public class P25Decoder extends Decoder implements Instrumentable
if( mCQPSKDemodulator != null )
{
mCQPSKDemodulator.addTap( tap );
mCQPSKDemodulator.removeTap( tap );
}
switch( tap.getName() )
@@ -361,11 +363,6 @@ public class P25Decoder extends Decoder implements Instrumentable
{
mC4FMSlicer.removeListener( slicerTap );
}
// if( mCQPSKSlicer != null )
// {
// mCQPSKSlicer.removeListener( slicerTap );
// }
break;
}
}

View File

@@ -31,6 +31,7 @@ import decode.p25.message.tsbk.TSBKMessage;
import decode.p25.message.tsbk.TSBKMessageFactory;
import decode.p25.reference.DataUnitID;
import dsp.psk.CQPSKDemodulator;
import dsp.psk.CQPSKDemodulator2;
import dsp.symbol.Dibit;
import dsp.symbol.FrameSync;
import edac.BCH_63_16_11;

View File

@@ -115,7 +115,7 @@ public class P25AudioOutput implements IAudioOutput, Listener<Message>,
}
catch ( ClassNotFoundException e1 )
{
mLog.error( "Couldn't find/load JMBE audio conversion library", e1 );
mLog.error( "Couldn't find/load JMBE audio conversion library" );
}
catch ( InstantiationException e1 )
{

View File

@@ -28,7 +28,9 @@ public class CMAEqualizer implements Listener<ComplexSample>
mModulus = modulus;
mMu = mu;
/* Set first tap to 1,0 as a start */
/* Set first tap to 1,0 and all other taps to 0,0 as a starting point.
* This means that the output is solely determined by the last sample
* entering the filter, initially. */
mTaps[ 0 ] = new ComplexSample( 1.0f, 0.0f );
for( int x = 1; x < TAP_COUNT; x++ )

View File

@@ -32,6 +32,7 @@ package dsp.psk;
import instrument.Instrumentable;
import instrument.tap.Tap;
import instrument.tap.stream.EyeDiagramData;
import instrument.tap.stream.EyeDiagramDataTap;
import java.text.DecimalFormat;
@@ -299,14 +300,6 @@ public class CQPSKDemodulator implements Instrumentable,
middleSymbol.normalize();
currentSymbol.normalize();
if( mEyeDiagramDataTap != null )
{
mEyeDiagramDataTap.receive(
new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
mDelayLinePointer, mDelayLinePointer + 20 ),
mMu, (float)half_sps + half_mu ) );
}
/* Gardner timing error calculations */
float errorInphase = ( mPreviousSymbol.inphase() -
currentSymbol.inphase() ) * middleSymbol.inphase();
@@ -316,9 +309,13 @@ public class CQPSKDemodulator implements Instrumentable,
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
// /*debug*/
// gardnerError = 0.0f;
if( mEyeDiagramDataTap != null )
{
mEyeDiagramDataTap.receive(
new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
0, 20 ), mMu, (float)half_sps + half_mu, gardnerError ) );
}
/* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */
mOmega = mOmega + mGainOmega * gardnerError;

View File

@@ -0,0 +1,561 @@
package dsp.psk;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014, 2015 Dennis Sheirer
*
* Gardner detector, Costas loop and Interpolator derived from:
*
* OP25 - gardner_costas_cc_impl.cc
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
*
* OP25 - scope.py
* Copyright 2008-2011 Steve Glass
* Copyright 2011, 2012, 2013 KA1RBI
*
* GNURadio - control_loop and mpsk_receiver_cc
* Copyright 2011,2013 Free Software Foundation, Inc.
*
* 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/>
******************************************************************************/
import instrument.Instrumentable;
import instrument.tap.Tap;
import instrument.tap.stream.EyeDiagramData;
import instrument.tap.stream.EyeDiagramDataTap;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.Provider;
import sample.complex.ComplexSample;
import source.tuner.frequency.FrequencyChangeListener;
import buffer.FloatAveragingBuffer;
import dsp.filter.interpolator.QPSKInterpolator;
import dsp.symbol.Dibit;
/**
* Implements a CQPSK demodulator using an early/late symbol timing error detector
* and a Costas Loop as a phase locked loop synchronized with the incoming
* signal carrier frequency.
*
* Sample Rate: 48000
* Symbol Rate: 4800
*/
public class CQPSKDemodulator2 implements Instrumentable,
Listener<ComplexSample>,
Provider<ComplexSample>
{
private final static Logger mLog =
LoggerFactory.getLogger( CQPSKDemodulator2.class );
/* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d );
/* 45 degree point */
public static final ComplexSample POINT_45_DEGREES =
new ComplexSample( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
private DecimalFormat mDecimalFormat = new DecimalFormat( "0.000000" );
private Listener<ComplexSample> mListener;
private EarlyLateSymbolTiming mGardnerDetector = new EarlyLateSymbolTiming();
private CostasLoop mCostasLoop = new CostasLoop();
private FrequencyChangeListener mFrequencyChangeListener;
private EyeDiagramDataTap mEyeDiagramDataTap;
public CQPSKDemodulator2()
{
}
@Override
public void receive( ComplexSample sample )
{
mGardnerDetector.receive( sample );
}
/**
* Applies a phase correction value to the costas loop to correct when a
* phase lock error is detected in the binary output stream.
*
* @param correction - value in radians
*/
public void correctPhaseError( double correction )
{
mCostasLoop.correctPhaseError( correction );
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip( float value, float maximum )
{
if( value > maximum )
{
return maximum;
}
else if( value < -maximum )
{
return -maximum;
}
return value;
}
/**
* Constrains timing error to +/- the maximum value and corrects any
* floating point invalid numbers
*/
private float normalize( float error, float maximum )
{
if( Float.isNaN( error ) )
{
return 0.0f;
}
else
{
return clip( error, maximum );
}
}
public void addListener( FrequencyChangeListener listener )
{
mFrequencyChangeListener = listener;
}
public void removeListener( FrequencyChangeListener listener )
{
mFrequencyChangeListener = null;
}
/**
* Processes the current loop frequency of the costas loop and broadcasts
* tuner frequency offset adjustments 10 times a second as needed to keep
* the frequency offset below half of the maximum frequency
*/
public class FrequencyControl
{
/* Set the trigger threshold at half of the maximum costas loop control
* frequency */
private static final float CORRECTION_THRESHOLD_FREQUENCY =
( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
private long mFrequencyError = 0;
private int mCounter = 0;
public void receive( float frequency )
{
// mCounter++;
//
// if( mCounter >= 480 )
// {
// float average = mBuffer.get( frequency );
// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY);
//
// boolean correctionNeeded = false;
//
// if( average > CORRECTION_THRESHOLD_FREQUENCY )
// {
// mFrequencyError--;
//
// correctionNeeded = true;
// }
// else if( average < -CORRECTION_THRESHOLD_FREQUENCY )
// {
// mFrequencyError++;
//
// correctionNeeded = true;
// }
//
// if( correctionNeeded && mFrequencyChangeListener != null )
// {
// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError );
// mFrequencyChangeListener.frequencyChanged(
// new FrequencyChangeEvent( Attribute.FREQUENCY_ERROR,
// mFrequencyError ) );
// }
//
// mCounter = 0;
// }
// else
// {
// mBuffer.get( frequency );
// }
}
}
/**
* Early/Late symbol timing error detector. Linear Simulcast Modulation (LSM)
* uses a Raised Cosine Filter at the transmitter which causes the
* signal magnitude to rise to unity at the optimal sampling point and drop
* to zero or a midpoint between unity and zero between each symbol period.
*
* This error detector continually adjusts symbol timing by continuously
* attempting to find the largest magnitude value between two interpolated
* sample points during each symbol period.
*/
public class EarlyLateSymbolTiming implements Listener<ComplexSample>
{
public static final int SAMPLES_PER_SYMBOL = 10;
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
private ComplexSample[] mDelayLine =
new ComplexSample[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
private int mDelayLinePointer = 0;
/* Sampling point */
private float mMu = 10.0f;
private float mGainMu = 0.5f;
/* Samples per symbol */
private float mOmega = 10.0f;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
private float mOmegaMid = 10.0f;
private QPSKInterpolator mInterpolator = new QPSKInterpolator( 1.0f );
private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f );
/**
* Provides symbol sampling timing control
*/
public EarlyLateSymbolTiming()
{
for( int x = 0; x < ( 2 * TWICE_SAMPLES_PER_SYMBOL ); x++ )
{
mDelayLine[ x ] = new ComplexSample( 0.0f, 0.0f );
}
}
@Override
public void receive( ComplexSample sample )
{
/* Count down samples per symbol until we calculate the symbol */
mMu--;
mCostasLoop.increment();
/* Mix incoming sample with costas loop to remove any rotation
* that is present from a mis-tuned carrier frequency */
ComplexSample derotatedSample = ComplexSample
.multiply( mCostasLoop.getCurrentVector(), sample );
/* Fill up the delay line to use with the interpolator */
mDelayLine[ mDelayLinePointer ] = derotatedSample;
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedSample;
/* Imcrement pointer and keep pointer in bounds */
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
/* Calculate the symbol once we've stored enough samples */
if( mMu <= 1.0f )
{
ComplexSample earlySample = mInterpolator
.filter( mDelayLine, mDelayLinePointer, mMu );
ComplexSample middleSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer + 1, mMu );
ComplexSample lateSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer + 2, mMu );
ComplexSample currentSymbol = ComplexSample.multiply(
middleSample, mPreviousSample.conjugate() );
/* Set symbol gain to unity */
currentSymbol.normalize();
float earlyLateError =
lateSample.magnitude() - earlySample.magnitude();
if( mEyeDiagramDataTap != null )
{
mEyeDiagramDataTap.receive(
new EyeDiagramData( Arrays.copyOfRange( mDelayLine, 0, 20 ),
(float)mDelayLinePointer + 4.0f + mMu,
(float)mDelayLinePointer + 4.0f + mMu + 0.25f, earlyLateError ) );
}
/* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */
mOmega = mOmega + mGainOmega * earlyLateError;
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
/* Adjust sample timing based on error of current sample */
mMu += mOmega + ( mGainMu * earlyLateError );
/* Store current samples/symbols to use for the next period */
mPreviousSample = middleSample;
/* Update costas loop using phase error present in current
* symbol. The symbol is rotated from star orientation to polar
* orientation to simplify error calculation */
mCostasLoop.receive( currentSymbol );
// mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
/* Dispatch the differentiated symbol to the registered listener */
if( mListener != null )
{
mListener.receive( currentSymbol );
}
}
}
}
/**
* Costas Loop - phase locked loop synchronized to the frequency offset of
* the incoming signal to enable the signal to be mixed down to zero offset
* for proper synchronization. The mFrequency value indicates the detected
* frequency offset. We attempt to keep that value close to zero by issuing
* frequency adjustments to the tuner channel source.
*
* Most of the costas loop code was ported from gnuradio/control_loop and
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
* OP25/cqpsk.py
*/
public class CostasLoop implements Listener<ComplexSample>
{
public static final double TWO_PI = 2.0 * Math.PI;
private static final float MAXIMUM_FREQUENCY =
( 2400.0f * (float)TWO_PI ) / 48000.0f;
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
/* Use denominator between 100 (most) and 400 (least) to adjust costas
* loop control level */
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
( mLoopBandwidth * mLoopBandwidth ) );
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
( mLoopBandwidth * mLoopBandwidth ) );
private float mLoopPhase = 0.0f;
private float mLoopFrequency = 0.0f;
private FrequencyControl mFrequencyControl = new FrequencyControl();
public CostasLoop()
{
}
/**
* Applies a phase correction value to the current phasor. Use this
* method to apply correction when a +/- 90, or 180 degree phase error
* is detected in the binary output stream.
*
* If the supplied correction value places the loop frequency outside
* of the max frequency, then the frequency will be corrected 360
* degrees in the opposite direction to maintain within the max
* frequency bounds.
*
* @param correction - correction value in radians
*/
public void correctPhaseError( double correction )
{
mLoopFrequency += correction;
if( mLoopFrequency > MAXIMUM_FREQUENCY )
{
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
}
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
{
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
}
}
/**
* Increments the phase of the loop
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Keep the loop phase in bounds */
unwrapPhase();
}
private void unwrapPhase()
{
while( mLoopPhase > TWO_PI )
{
mLoopPhase -= TWO_PI;
}
while( mLoopPhase < -TWO_PI )
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop rotated by THETA degrees.
*/
public ComplexSample getCurrentVector()
{
// return ComplexSample.fromAngle( mLoopPhase + THETA );
return ComplexSample.fromAngle( mLoopPhase );
}
@Override
public void receive( ComplexSample sample )
{
/* Calculate phase error */
float phaseError = getPhaseError( sample );
Dibit d = QPSKPolarSlicer.decide( sample );
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
/* Adjust for phase error */
adjust( phaseError );
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phase_error - (-)= late and (+)= early
*/
private void adjust( float phase_error )
{
mLoopFrequency += mBetaGain * phase_error;
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
/* Maintain phase between +/- 2 * PI */
unwrapPhase();
/* Limit frequency to +/- maximum loop frequency */
limitFrequency();
}
/**
* Constrains the frequency within the bounds of +/- loop frequency
*/
private void limitFrequency()
{
/* Check for and issue tuner offset correction */
mFrequencyControl.receive( mLoopFrequency );
if( mLoopFrequency > MAXIMUM_FREQUENCY )
{
mLoopFrequency = MAXIMUM_FREQUENCY;
}
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
{
mLoopFrequency = -MAXIMUM_FREQUENCY;
}
}
/**
* Detects rotational error present when the costas loop is not aligned
* with the carrier frequency. Provides error feedback to adjust
* mixer frequency.
*/
public float getPhaseError( ComplexSample sample )
{
float phase_error = 0;
if( Math.abs( sample.real() ) > Math.abs( sample.imaginary() ) )
{
if( sample.real() > 0 )
{
phase_error = -sample.imaginary();
}
else
{
phase_error = sample.imaginary();
}
}
else
{
if( sample.imaginary() > 0 )
{
phase_error = sample.real();
}
else
{
phase_error = -sample.real();
}
}
return phase_error;
}
}
@Override
public void setListener( Listener<ComplexSample> listener )
{
mListener = listener;
}
@Override
public void removeListener( Listener<ComplexSample> listener )
{
mListener = null;
}
@Override
public List<Tap> getTaps()
{
List<Tap> taps = new ArrayList<Tap>();
taps.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
return taps;
}
@Override
public void addTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
}
}
@Override
public void removeTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = null;
}
}
}

View File

@@ -1,33 +0,0 @@
package dsp.psk;
import sample.complex.ComplexSample;
public class EyeDiagramData
{
private ComplexSample[] mSamples;
private float mSamplePoint;
private float mMiddlePoint;
public EyeDiagramData( ComplexSample[] samples, float samplePoint, float middlePoint )
{
mSamples = samples;
mSamplePoint = samplePoint;
mMiddlePoint = middlePoint;
}
public ComplexSample[] getSamples()
{
return mSamples;
}
public float getSamplePoint()
{
return mSamplePoint;
}
public float getMiddlePoint()
{
return mMiddlePoint;
}
}

View File

@@ -17,11 +17,18 @@
******************************************************************************/
package instrument.gui;
import java.awt.AWTException;
import java.awt.EventQueue;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.imageio.ImageIO;
import javax.swing.JDesktopPane;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
@@ -34,10 +41,12 @@ import javax.swing.filechooser.FileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import source.Source;
import source.wave.ComplexWaveSource;
import source.wave.FloatWaveSource;
import source.wave.WaveSource;
import util.TimeStamp;
public class Viewer
{
@@ -101,24 +110,67 @@ public class Viewer
fileMenu.add( exitMenu );
// JMenuItem windowItem = new JMenuItem( "New Window" );
// windowItem.addActionListener( new ActionListener()
// {
// @Override
// public void actionPerformed( ActionEvent arg0 )
// {
// JInternalFrame frame =
// new JInternalFrame( "New", true, true, true, true );
//
// frame.add( new JLabel( "This is a test" ) );
// frame.pack();
//
// mDesktop.add( frame );
//
// frame.setVisible( true );
// }
// } );
// menuBar.add( windowItem );
JMenuItem screenCaptureItem = new JMenuItem( "Screen Capture" );
screenCaptureItem.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
try
{
Robot robot = new Robot();
final BufferedImage image =
robot.createScreenCapture( mFrame.getBounds() );
SystemProperties props = SystemProperties.getInstance();
Path capturePath = props.getApplicationFolder( "screen_captures" );
if( !Files.exists( capturePath ) )
{
try
{
Files.createDirectory( capturePath );
}
catch ( IOException e )
{
mLog.error( "Couldn't create 'screen_captures' "
+ "subdirectory in the " +
"SDRTrunk application directory", e );
}
}
String filename = TimeStamp.getTimeStamp( "_" ) +
"_screen_capture.png";
final Path captureFile = capturePath.resolve( filename );
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
try
{
ImageIO.write( image, "png",
captureFile.toFile() );
}
catch ( IOException e )
{
mLog.error( "Couldn't write screen capture to "
+ "file [" + captureFile.toString() + "]", e );
}
}} );
}
catch ( AWTException e )
{
mLog.error( "Exception while taking screen capture", e );
}
}
} );
menuBar.add( screenCaptureItem );
}
private void setSourceFile( File file )

View File

@@ -0,0 +1,40 @@
package instrument.tap.stream;
import sample.complex.ComplexSample;
public class EyeDiagramData
{
private ComplexSample[] mSamples;
private float mLeftPoint;
private float mRightPoint;
private float mError;
public EyeDiagramData( ComplexSample[] samples, float leftPoint,
float rightPoint, float error )
{
mSamples = samples;
mLeftPoint = leftPoint;
mRightPoint = rightPoint;
mError = error;
}
public ComplexSample[] getSamples()
{
return mSamples;
}
public float getLeftPoint()
{
return mLeftPoint;
}
public float getRightPoint()
{
return mRightPoint;
}
public float getError()
{
return mError;
}
}

View File

@@ -17,13 +17,18 @@
******************************************************************************/
package instrument.tap.stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import decode.p25.P25Decoder;
import instrument.tap.TapListener;
import instrument.tap.TapType;
import sample.Listener;
import dsp.psk.EyeDiagramData;
public class EyeDiagramDataTap extends StreamTap implements Listener<EyeDiagramData>
{
private final static Logger mLog = LoggerFactory.getLogger( EyeDiagramDataTap.class );
private Listener<EyeDiagramData> mListener;
public EyeDiagramDataTap( String name, int delay, float sampleRate )

View File

@@ -33,7 +33,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.complex.ComplexSample;
import dsp.psk.EyeDiagramData;
public class EyeDiagramDataTapPanel extends TapViewPanel
{
@@ -69,135 +68,158 @@ public class EyeDiagramDataTapPanel extends TapViewPanel
Graphics2D g2 = (Graphics2D)g;
int min = ( getHeight() < getWidth() ? getHeight() : getWidth() );
float halfWidth = getWidth() / 2.0f;
float third = getWidth() / 3.0f;
float two_thirds = 2.0f * third;
float halfHeight = getHeight() / 2.0f;
float xAxisIncrement = getWidth() / 19.0f / 4.0f;
float middle = 6.0f * xAxisIncrement;
g2.setColor( Color.BLUE );
g2.drawString( "Eye Diagram - I Leg", 5 , 15 );
g2.drawString( "Eye Diagram - Q Leg", halfWidth , 15 );
float xAxisIncrement = third / 20.0f;
/* Vertical Line */
g2.drawLine( getWidth() / 8, 5, getWidth() / 8, getHeight() - 5 );
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) ), 5,
(int)( halfWidth + ( getWidth() / 8 ) ), getHeight() - 5 );
g2.setColor( Color.LIGHT_GRAY );
g2.drawLine( (int)( getWidth() / 8 - xAxisIncrement ), 5, (int)( getWidth() / 8 - xAxisIncrement ), getHeight() - 5 );
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), 5,
(int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), getHeight() - 5 );
g2.drawLine( (int)( getWidth() / 8 + xAxisIncrement ), 5, (int)( getWidth() / 8 + xAxisIncrement ), getHeight() - 5 );
g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), 5,
(int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), getHeight() - 5 );
/* Horizontal Line */
g2.drawLine( 0, getHeight() / 2, getWidth() / 4, getHeight() / 2 );
g2.drawLine( (int)halfWidth, getHeight() / 2, (int)(getWidth() * .75),
getHeight() / 2 );
if( mSamples != null &&
mSamples.size() > 0 )
if( mSamples != null && mSamples.size() > 0 )
{
g2.setColor( Color.BLUE );
/* Draw each of the sample sets */
for( int z = 0; z < mSamples.size(); z++ )
{
EyeDiagramData data = mSamples.get( z );
if( z < mSamples.size() * .75 )
{
g2.setColor( Color.LIGHT_GRAY );
}
else if( z == mSamples.size() - 1 )
if( z == mSamples.size() - 1 )
{
g2.setColor( Color.BLUE );
}
else if( z > ( mSamples.size() * .75 ) )
{
g2.setColor( Color.DARK_GRAY );
}
else
{
g2.setColor( Color.GRAY );
g2.setColor( Color.LIGHT_GRAY );
}
EyeDiagramData data = mSamples.get( z );
ComplexSample[] samples = data.getSamples();
Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement );
iLine.moveTo( xAxis, halfHeight );
qLine.moveTo( xAxis + halfWidth, halfHeight );
for( int x = 0; x < samples.length; x++ )
/* Draw the sample set containing two symbol periods of data */
if( samples.length >= 1 )
{
float i = samples[ x ].inphase();
Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
Path2D.Float mLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
if( i > 1.0 )
float xAxis = 0;
iLine.moveTo( xAxis, getSampleHeight( halfHeight, samples[ 0 ].inphase() ) );
qLine.moveTo( xAxis + third, getSampleHeight( halfHeight, samples[ 0 ].quadrature() ) );
mLine.moveTo( xAxis + ( two_thirds ), getMagnitudeHeight( samples[ 0 ].magnitude() ) );
for( int x = 1; x < samples.length; x++ )
{
i = 1.0f;
}
if( i < -1.0 )
{
i = -1.0f;
xAxis += xAxisIncrement;
iLine.lineTo( xAxis, getSampleHeight( halfHeight, samples[ x ].inphase() ) );
qLine.lineTo( xAxis + third, getSampleHeight( halfHeight, samples[ x ].quadrature() ) );
mLine.lineTo( xAxis + two_thirds, getMagnitudeHeight( samples[ x ].magnitude() ) );
}
float iy = halfHeight + ( .85f * ( halfHeight * i ) );
g2.draw( iLine );
g2.draw( qLine );
g2.draw( mLine );
iLine.lineTo( xAxis, iy );
float q = samples[ x ].quadrature();
if( q > 1.0 )
/* Left Sample line */
if( z == mSamples.size() - 1 )
{
q = 1.0f;
g2.setColor( Color.GREEN );
}
if( q < -1.0 )
else
{
q = -1.0f;
g2.setColor( Color.LIGHT_GRAY );
}
xAxis = xAxisIncrement * data.getLeftPoint();
g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 );
g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 );
g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 );
/* Right Sample line */
if( z == mSamples.size() - 1 )
{
g2.setColor( Color.GREEN );
}
else
{
g2.setColor( Color.LIGHT_GRAY );
}
xAxis = xAxisIncrement * data.getRightPoint();
g2.drawLine( (int)xAxis, 5, (int)xAxis, getHeight() - 5 );
g2.drawLine( (int)( xAxis + third ), 5, (int)( xAxis + third ), getHeight() - 5 );
g2.drawLine( (int)( xAxis + two_thirds ), 5, (int)( xAxis + two_thirds ), getHeight() - 5 );
/* Error indicator */
float error = data.getError();
if( z == mSamples.size() - 1 )
{
g2.setColor( Color.BLACK );
float xFrom;
float xTo;
if( error < 0.0f )
{
xFrom = (int)( xAxisIncrement * data.getLeftPoint() );
xTo = xFrom - 25.0f;
}
else
{
xFrom = (int)( xAxisIncrement * data.getRightPoint() );
xTo = xFrom + 25.0f;
}
g2.drawLine( (int)xFrom, (int)halfHeight - 15, (int)xTo, (int)halfHeight );
g2.drawLine( (int)xFrom, (int)halfHeight + 15, (int)xTo, (int)halfHeight );
g2.drawLine( (int)( xFrom + third ), (int)halfHeight - 15, (int)( xTo + third ), (int)halfHeight );
g2.drawLine( (int)( xFrom + third ), (int)halfHeight + 15, (int)( xTo + third ), (int)halfHeight );
g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight - 15, (int)( xTo + two_thirds ), (int)halfHeight );
g2.drawLine( (int)( xFrom + two_thirds ), (int)halfHeight + 15, (int)( xTo + two_thirds ), (int)halfHeight );
}
float qy = halfHeight + ( .85f * ( halfHeight * q ) );
qLine.lineTo( xAxis + halfWidth, qy );
xAxis += xAxisIncrement;
}
g2.draw( iLine );
g2.draw( qLine );
}
}
if( mSamples.size() > 0 )
{
EyeDiagramData data = mSamples.get( mSamples.size() - 1 );
g2.setColor( Color.BLUE );
g2.drawString( "Eye Diagram - I Leg", 5, 15 );
g2.drawString( "Eye Diagram - Q Leg", third, 15 );
g2.drawString( "Eye Diagram - Magnitude", two_thirds, 15 );
g2.setColor( Color.GREEN );
float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement );
Path2D.Float magnitude = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 );
magnitude.moveTo( xAxis, halfHeight );
for( ComplexSample sample: data.getSamples() )
{
magnitude.lineTo( xAxis, getHeight() - ( getHeight() * sample.magnitude() ) );
xAxis += xAxisIncrement;
}
g2.setStroke( new BasicStroke( 3 ) );
g2.draw( magnitude );
}
/* Horizontal middle line */
g2.setColor( Color.CYAN );
g2.drawLine( 0, (int)halfHeight, getWidth(), (int)halfHeight );
g2.dispose();
}
private float getSampleHeight( float halfHeight, float value )
{
if( value > 1.0 )
{
value = 1.0f;
}
if( value < -1.0 )
{
value = -1.0f;
}
return halfHeight + ( .95f * ( halfHeight * value ) );
}
private float getMagnitudeHeight( float magnitude )
{
if( magnitude > 1.0 )
{
magnitude = 1.0f;
}
return getHeight() - ( getHeight() * magnitude );
}
}