diff --git a/src/decode/p25/P25Decoder.java b/src/decode/p25/P25Decoder.java index 57b3c89e..cfd96a66 100644 --- a/src/decode/p25/P25Decoder.java +++ b/src/decode/p25/P25Decoder.java @@ -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; } } diff --git a/src/decode/p25/P25MessageFramer.java b/src/decode/p25/P25MessageFramer.java index 033a3a84..fcb90263 100644 --- a/src/decode/p25/P25MessageFramer.java +++ b/src/decode/p25/P25MessageFramer.java @@ -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; diff --git a/src/decode/p25/audio/P25AudioOutput.java b/src/decode/p25/audio/P25AudioOutput.java index 51af5cb2..da4bf084 100644 --- a/src/decode/p25/audio/P25AudioOutput.java +++ b/src/decode/p25/audio/P25AudioOutput.java @@ -115,7 +115,7 @@ public class P25AudioOutput implements IAudioOutput, Listener, } 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 ) { diff --git a/src/dsp/filter/equalizer/CMAEqualizer.java b/src/dsp/filter/equalizer/CMAEqualizer.java index 4d1ead28..882367f6 100644 --- a/src/dsp/filter/equalizer/CMAEqualizer.java +++ b/src/dsp/filter/equalizer/CMAEqualizer.java @@ -28,7 +28,9 @@ public class CMAEqualizer implements Listener 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++ ) diff --git a/src/dsp/psk/CQPSKDemodulator.java b/src/dsp/psk/CQPSKDemodulator.java index 19a9c2fc..94aa97de 100644 --- a/src/dsp/psk/CQPSKDemodulator.java +++ b/src/dsp/psk/CQPSKDemodulator.java @@ -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; diff --git a/src/dsp/psk/CQPSKDemodulator2.java b/src/dsp/psk/CQPSKDemodulator2.java new file mode 100644 index 00000000..1b0fe271 --- /dev/null +++ b/src/dsp/psk/CQPSKDemodulator2.java @@ -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 + ******************************************************************************/ + +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, + Provider +{ + 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 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 + { + 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 + { + 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 listener ) + { + mListener = listener; + } + + @Override + public void removeListener( Listener listener ) + { + mListener = null; + } + + @Override + public List getTaps() + { + List taps = new ArrayList(); + + 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; + } + } +} diff --git a/src/dsp/psk/EyeDiagramData.java b/src/dsp/psk/EyeDiagramData.java deleted file mode 100644 index 04c3ea73..00000000 --- a/src/dsp/psk/EyeDiagramData.java +++ /dev/null @@ -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; - } -} diff --git a/src/instrument/gui/Viewer.java b/src/instrument/gui/Viewer.java index 7d0f23e6..d7808b58 100644 --- a/src/instrument/gui/Viewer.java +++ b/src/instrument/gui/Viewer.java @@ -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 ) diff --git a/src/instrument/tap/stream/EyeDiagramData.java b/src/instrument/tap/stream/EyeDiagramData.java new file mode 100644 index 00000000..143269c3 --- /dev/null +++ b/src/instrument/tap/stream/EyeDiagramData.java @@ -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; + } +} diff --git a/src/instrument/tap/stream/EyeDiagramDataTap.java b/src/instrument/tap/stream/EyeDiagramDataTap.java index 3888aeb4..5358806a 100644 --- a/src/instrument/tap/stream/EyeDiagramDataTap.java +++ b/src/instrument/tap/stream/EyeDiagramDataTap.java @@ -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 { + private final static Logger mLog = LoggerFactory.getLogger( EyeDiagramDataTap.class ); + private Listener mListener; public EyeDiagramDataTap( String name, int delay, float sampleRate ) diff --git a/src/instrument/tap/stream/EyeDiagramDataTapPanel.java b/src/instrument/tap/stream/EyeDiagramDataTapPanel.java index 784767cc..9edbc6fd 100644 --- a/src/instrument/tap/stream/EyeDiagramDataTapPanel.java +++ b/src/instrument/tap/stream/EyeDiagramDataTapPanel.java @@ -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 ); + } }