diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index fc474e43..3721dbce 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -14,6 +14,7 @@ nutall overflowable polyphase + qpsk remez resampler samplerate diff --git a/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java b/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java index 52acb572..655e6d6f 100644 --- a/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java +++ b/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java @@ -1,6 +1,26 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.bits; public interface ISyncProcessor { - public void checkSync( long value ); + /** + * Checks if the value matches a known sync pattern + * @param value + * @return + */ + boolean checkSync(long value); } diff --git a/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java b/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java index c313b484..4b4e8621 100644 --- a/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java +++ b/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java @@ -1,111 +1,130 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.bits; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; /** - * Multi-Sync pattern matcher. Accepts multiple sync detector objects for + * Multi-Sync pattern matcher. Accepts multiple sync detector objects for * comparision against the incoming bit stream. - * + * * Note: works for sync patterns up to 63 bits (integer size - 1 ) long. * If a 64 bit or larger sync pattern is applied, then the wrapping feature * of the Long.rotate methods will wrap the MSB around to the LSB and * corrupt the matching value. - * */ public class MultiSyncPatternMatcher { - private List mSyncProcessors = new ArrayList(); - - private long mBits = 0; - private long mMask = 0; - - public MultiSyncPatternMatcher( int syncSize ) - { - Validate.isTrue(syncSize < 64); - - //Setup a bit mask of all ones the length of the sync pattern - mMask = (long)( ( Math.pow( 2, syncSize ) ) - 1 ); - } - - public void dispose() - { - mSyncProcessors.clear(); - } - - /** - * Processes two bits before checking sync processors for a match. - */ - public void receive( boolean bit1, boolean bit2 ) - { - mBits = Long.rotateLeft( mBits, 1 ); - - mBits &= mMask; - - if( bit1 ) - { - mBits += 1; - } + private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class); - mBits = Long.rotateLeft( mBits, 1 ); - - mBits &= mMask; - - if( bit2 ) - { - mBits += 1; - } + private ISyncDetectListener mSyncDetectListener; + private int mSyncLossThreshold; + private int mBitCount; + private List mSyncProcessors = new ArrayList(); - for( ISyncProcessor processor: mSyncProcessors ) - { - processor.checkSync( mBits ); - } - } - - /** - * Processes one bit before checking sync processors for a match. - */ - public void receive( boolean bit ) - { - mBits = Long.rotateLeft( mBits, 1 ); - - mBits &= mMask; - - if( bit ) - { - mBits += 1; - } + private long mBits = 0; + private long mMask = 0; - for( ISyncProcessor processor: mSyncProcessors ) - { - processor.checkSync( mBits ); - } - } + public MultiSyncPatternMatcher(ISyncDetectListener syncDetectListener, int syncLossThreshold, int syncSize) + { + mSyncDetectListener = syncDetectListener; + mSyncLossThreshold = syncLossThreshold; + Validate.isTrue(syncSize < 64); - /** - * Adds a sync processor to receive the bit stream. - */ - public void add( ISyncProcessor processor ) - { - mSyncProcessors.add( processor ); - } + //Setup a bit mask of all ones the length of the sync pattern + mMask = (long)((Math.pow(2, syncSize)) - 1); + } + + public void dispose() + { + mSyncProcessors.clear(); + } + + /** + * Processes two bits before checking sync processors for a match. + */ + public void receive(boolean bit1, boolean bit2) + { + mBits = Long.rotateLeft(mBits, 1); + + mBits &= mMask; + + if(bit1) + { + mBits += 1; + } + + mBits = Long.rotateLeft(mBits, 1); + + mBits &= mMask; + + if(bit2) + { + mBits += 1; + } + + mBitCount += 2; + + for(ISyncProcessor processor : mSyncProcessors) + { + if(processor.checkSync(mBits)) + { + mSyncDetectListener.syncDetected(); + mBitCount = 0; + } + } + + if(mBitCount > mSyncLossThreshold) + { + mBitCount = 0; + mSyncDetectListener.syncLost(); + } + } + + /** + * Processes one bit before checking sync processors for a match. + */ + public void receive(boolean bit) + { + mBits = Long.rotateLeft(mBits, 1); + + mBits &= mMask; + + if(bit) + { + mBits += 1; + } + + for(ISyncProcessor processor : mSyncProcessors) + { + processor.checkSync(mBits); + } + } + + /** + * Adds a sync processor to receive the bit stream. + */ + public void add(ISyncProcessor processor) + { + mSyncProcessors.add(processor); + } } diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java index c2748f83..6362e160 100644 --- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java +++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java @@ -1,50 +1,68 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.bits; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; public class SoftSyncDetector implements ISyncProcessor { - private ISyncDetectListener mListener; - private long mPattern; - private int mThreshold; - - public SoftSyncDetector( ISyncDetectListener listener, long pattern, int threshold ) - { - this( pattern, threshold ); - - mListener = listener; - } - - public SoftSyncDetector( long pattern, int threshold ) - { - mPattern = pattern; - mThreshold = threshold; - } - - public void dispose() - { - mListener = null; - } + private ISyncDetectListener mListener; + private long mPattern; + private int mThreshold; - @Override - public void checkSync( long value ) - { - long difference = value ^ mPattern; - - if( ( difference == 0 || Long.bitCount( difference ) <= mThreshold ) && - mListener != null ) - { - mListener.syncDetected(); - } - } - - public void setThreshold( int threshold ) - { - mThreshold = threshold; - } - - public void setListener( ISyncDetectListener listener ) - { - mListener = listener; - } + public SoftSyncDetector(ISyncDetectListener listener, long pattern, int threshold) + { + this(pattern, threshold); + + mListener = listener; + } + + public SoftSyncDetector(long pattern, int threshold) + { + mPattern = pattern; + mThreshold = threshold; + } + + public void dispose() + { + mListener = null; + } + + @Override + public boolean checkSync(long value) + { + long difference = value ^ mPattern; + + if((difference == 0 || Long.bitCount(difference) <= mThreshold) && + mListener != null) + { + mListener.syncDetected(); + return true; + } + + return false; + } + + public void setThreshold(int threshold) + { + mThreshold = threshold; + } + + public void setListener(ISyncDetectListener listener) + { + mListener = listener; + } } diff --git a/src/main/java/io/github/dsheirer/bits/SyncDetector.java b/src/main/java/io/github/dsheirer/bits/SyncDetector.java index 23aa6be0..aedaceec 100644 --- a/src/main/java/io/github/dsheirer/bits/SyncDetector.java +++ b/src/main/java/io/github/dsheirer/bits/SyncDetector.java @@ -1,35 +1,57 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.bits; import io.github.dsheirer.dsp.symbol.ISyncDetectListener; public class SyncDetector implements ISyncProcessor { - private long mPattern; - private ISyncDetectListener mListener; + private long mPattern; + private ISyncDetectListener mListener; - public SyncDetector( long pattern ) - { - mPattern = pattern; - } - - public SyncDetector( long pattern, ISyncDetectListener listener ) - { - this( pattern ); - - mListener = listener; - } - - public void setListener( ISyncDetectListener listener ) - { - mListener = listener; - } + public SyncDetector(long pattern) + { + mPattern = pattern; + } - @Override - public void checkSync( long value ) - { - if( value == mPattern && mListener != null ) - { - mListener.syncDetected(); - } - } + public SyncDetector(long pattern, ISyncDetectListener listener) + { + this(pattern); + + mListener = listener; + } + + public void setListener(ISyncDetectListener listener) + { + mListener = listener; + } + + @Override + public boolean checkSync(long value) + { + if(value == mPattern) + { + if(mListener != null) + { + mListener.syncDetected(); + } + + return true; + } + + return false; + } } diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java index f2d069d5..bc71e4f1 100644 --- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java @@ -35,17 +35,16 @@ import io.github.dsheirer.module.decode.event.ICallEventProvider; import io.github.dsheirer.sample.IOverflowListener; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProvider; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.heartbeat.Heartbeat; import io.github.dsheirer.source.heartbeat.IHeartbeatListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; - public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, - IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider, - IAttributeChangeRequestListener, IOverflowListener + IDecoderStateEventProvider, ISourceEventListener, ISourceEventProvider, IHeartbeatListener, + ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener { private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); @@ -57,12 +56,13 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder private Listener mCallEventListener; private Listener mDecoderStateListener; private Listener mSquelchStateListener; + private Listener mExternalSourceEventListener; private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver(); private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver(); private ChannelType mChannelType; private TrafficChannelManager mTrafficChannelEndListener; private CallEvent mTrafficChannelCallEvent; - private SourceChangeListener mSourceChangeListener; + private SourceEventListener mInternalSourceEventListener; private boolean mSquelchLocked = false; private boolean mSelected = false; @@ -160,12 +160,12 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder @Override public Listener getSourceEventListener() { - if(mSourceChangeListener == null) + if(mInternalSourceEventListener == null) { - mSourceChangeListener = new SourceChangeListener(); + mInternalSourceEventListener = new SourceEventListener(); } - return mSourceChangeListener; + return mInternalSourceEventListener; } private boolean isStandardChannel() @@ -255,6 +255,17 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } } + /** + * Broadcasts the source event to a registered external source event listener + */ + protected void broadcast(SourceEvent sourceEvent) + { + if(mExternalSourceEventListener != null) + { + mExternalSourceEventListener.receive(sourceEvent); + } + } + /** * Sets the squelch state listener */ @@ -516,18 +527,46 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder } /** - * Listener to receive frequency change events and rebroadcast them to the decoder states + * Registers the listener to receive source events from the channel state */ - public class SourceChangeListener implements Listener + @Override + public void setSourceEventListener(Listener listener) + { + mExternalSourceEventListener = listener; + } + + /** + * De-Registers a listener from receiving source events from the channel state + */ + @Override + public void removeSourceEventListener() + { + mExternalSourceEventListener = null; + } + + /** + * Listener to receive source events. + */ + public class SourceEventListener implements Listener { @Override public void receive(SourceEvent sourceEvent) { - if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE) + switch(sourceEvent.getEvent()) { - long frequency = sourceEvent.getValue().longValue(); - - broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency)); + case NOTIFICATION_FREQUENCY_CHANGE: + //Rebroadcast source frequency change events for the decoder(s) to process + long frequency = sourceEvent.getValue().longValue(); + broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency)); + break; + case NOTIFICATION_MEASURED_FREQUENCY_ERROR: + //Rebroadcast frequency error measurements to external listener if we're currently + //in an active (ie sync locked) state. + if(getState().isActiveState()) + { + broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannelType.name())); + } + break; } } } diff --git a/src/main/java/io/github/dsheirer/channel/state/State.java b/src/main/java/io/github/dsheirer/channel/state/State.java index d6e5b09f..a4047e7f 100644 --- a/src/main/java/io/github/dsheirer/channel/state/State.java +++ b/src/main/java/io/github/dsheirer/channel/state/State.java @@ -109,6 +109,17 @@ public enum State public abstract boolean canChangeTo(State state); + /** + * Indicates that this state is an active (ie decoding) state and is one of the states + * enumerated in the CALL_STATES enumeration set. + * + * @return true if this state is an active state + */ + public boolean isActiveState() + { + return CALL_STATES.contains(this); + } + public String getDisplayValue() { return mDisplayValue; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 8be164e1..a569117e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -556,6 +556,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: //ignore break; + case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED: + //Rebroadcast so that the tuner source can process this event + mSourceEventBroadcaster.broadcast(sourceEvent); + break; default: mLog.error("Received unrecognized source event from polyphase channel source [" + sourceEvent.getEvent() + "]"); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java deleted file mode 100644 index ce46030c..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java +++ /dev/null @@ -1,171 +0,0 @@ -/******************************************************************************* - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.dsp.filter.channelizer; - -import java.util.Arrays; - -public class PolyphaseChannelResultsBuffer -{ - private long mTimestamp; - private int mPointer = 0; - private int mMaxSize; - private float[][] mChannelResults; - - /** - * Buffer containing an array of polyphase channelizer complex channel results. - * @param timestamp reference relative to the first channel results. - * @param maxSize of channel results to store in this buffer - */ - public PolyphaseChannelResultsBuffer(long timestamp, int maxSize) - { - mTimestamp = timestamp; - mMaxSize = maxSize; - } - - /** - * Reference timestamp relative to the first channel results contained in this buffer. - * @return reference timestamp in milliseconds since epoch - */ - public long getTimestamp() - { - return mTimestamp; - } - - /** - * Indicates if this buffer is full - */ - public boolean isFull() - { - return mPointer >= mMaxSize; - } - - /** - * Indicates if this buffer is empty - */ - public boolean isEmpty() - { - return mPointer == 0; - } - - /** - * Adds the channel results to this buffer - * @param channelResults to add - * @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different - * length than any of the preceeding channel results that were added to this buffer - */ - public void add(float[] channelResults) throws IllegalArgumentException - { - if(mPointer >= mMaxSize) - { - throw new IllegalArgumentException("Buffer is full - cannot add new channel results"); - } - - if(mPointer == 0) - { - mChannelResults = new float[mMaxSize][channelResults.length]; - } - else - { - int currentLength = mChannelResults[0].length; - - if(channelResults.length != currentLength) - { - throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " + - "a different length than the channel results that are already contained in this buffer [" + - currentLength + "]"); - } - } - - mChannelResults[mPointer++] = channelResults; - } - - /** - * Channel results array contained in this buffer - */ - public float[][] getChannelResults() - { - //If this buffer is full, return all of the channel results - if(mPointer == mMaxSize) - { - return mChannelResults; - } - //Otherwise, truncate the results to return - else - { - return Arrays.copyOf(mChannelResults, mPointer); - } - } - - /** - * Extracts a single I/Q interleaved sample buffer from the channel results - * @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i. - * @return float[] containing interleaved I and Q samples - * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is - * not within the channel range of the contained channel results. - */ - public float[] getChannel(int iChannelIndex) - { - if(isEmpty()) - { - throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]"); - } - - if(!isValidChannelIndex(iChannelIndex)) - { - throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " + - "results -- max channel is " + getMaxChannelIndex()); - } - - float[] samples = new float[mChannelResults.length * 2]; - int pointer = 0; - - int qChannelIndex = iChannelIndex + 1; - - for(int x = 0; x < mPointer; x++) - { - float[] channelResults = mChannelResults[x]; - - samples[pointer++] = channelResults[iChannelIndex]; - samples[pointer++] = channelResults[qChannelIndex]; - } - - return samples; - } - - /** - * Indicates if the channel argument is valid for the contained channel results. - * @param channelIndex number (0 to N) to check - * @return false if the buffer is empty or if the channel is greater than the contained channel results - */ - private boolean isValidChannelIndex(int channelIndex) - { - return channelIndex <= getMaxChannelIndex(); - } - - /** - * Maximum channel index represented in the channel results contained in this buffer. - * @return max channel number, or 0 if the buffer is empty - */ - private int getMaxChannelIndex() - { - if(isEmpty()) - { - return 0; - } - - return mChannelResults[0].length; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 222e0690..1872fb96 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -237,4 +237,11 @@ public class PolyphaseChannelSource extends TunerChannelSource mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); } } + + @Override + public String toString() + { + return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " + + getTunerChannel().getFrequency(); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 599832fb..ec27da9a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -62,6 +62,12 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5)); } + @Override + public int getPolyphaseChannelIndexCount() + { + return mInputChannelCount; + } + public void dispose() { if(mChannelResultsQueue != null) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index 70eb7c4a..3b88b07e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -59,6 +59,11 @@ public interface IPolyphaseChannelOutputProcessor */ void setPolyphaseChannelIndices(List indexes); + /** + * List of current polyphase channel indices for the output processor + */ + int getPolyphaseChannelIndexCount(); + /** * Updates the synthesis filter taps for this output processor * @param filter diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java index 2317af18..cde125aa 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java +++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java @@ -134,6 +134,12 @@ public class LTRDecoder implements Listener, ISyncDetectListener mSynchronizationMonitor.syncDetected(); } + @Override + public void syncLost() + { + //no-op + } + /** * Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is * greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java index 1608a3d6..7d9746b7 100644 --- a/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java +++ b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java @@ -30,13 +30,13 @@ import org.slf4j.LoggerFactory; * * Synchronization States: * - * -COARSE - default state. demoted from MEDIUM after one message period with no sync detection. + * -COARSE - default state. demoted from MEDIUM after one message period with no sync detection. * - * -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE - * after two message periods with no sync detections. + * -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE + * after two message periods with no sync detections. * - * -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more - * message periods. + * -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more + * message periods. */ public class SynchronizationMonitor implements ISyncDetectListener { @@ -50,6 +50,7 @@ public class SynchronizationMonitor implements ISyncDetectListener /** * Constructs the synchronization monitor for the specified message symbol length. + * * @param messageLength in symbols. */ public SynchronizationMonitor(int messageLength) @@ -127,6 +128,12 @@ public class SynchronizationMonitor implements ISyncDetectListener updateSyncState(); } + @Override + public void syncLost() + { + //no-op + } + /** * Increments the symbol count for each decoded symbol. */ diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java new file mode 100644 index 00000000..eb48da17 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java @@ -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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk.pll; + +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.module.decode.p25.P25Decoder; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Monitors decode sync events to adaptively control PLL loop gain. + */ +public class AdaptivePLLGainMonitor implements ISyncDetectListener, IFrequencyErrorProcessor +{ + private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLGainMonitor.class); + + private static final int MAX_SYNC_COUNT = 6; + private CostasLoop mCostasLoop; + private P25Decoder mP25Decoder; + private PLLGain mPLLGain = PLLGain.LEVEL_1; + private int mSyncCount; + + /** + * Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain + * level of the costas loop accordingly. + * + * @param costasLoop to receive adaptive gain updates. + * @param p25Decoder to receive frequency error source events + */ + public AdaptivePLLGainMonitor(CostasLoop costasLoop, P25Decoder p25Decoder) + { + mCostasLoop = costasLoop; + mCostasLoop.setPLLGain(mPLLGain); + mCostasLoop.setFrequencyErrorProcessor(this); + mP25Decoder = p25Decoder; + } + + /** + * Sync detection event. Updates the running sync count and updates the PLL gain level. + */ + @Override + public void syncDetected() + { + mSyncCount++; + update(); + } + + /** + * Sync loss event. Updates the running sync count and updates the PLL gain level. + */ + @Override + public void syncLost() + { + mSyncCount -= 2; + update(); + } + + /** + * Updates the gain state for a sync detect or sync loss + */ + private void update() + { + if(mSyncCount < 0) + { + mSyncCount = 0; + return; + } + + if(mSyncCount > MAX_SYNC_COUNT) + { + mSyncCount = MAX_SYNC_COUNT; + return; + } + + PLLGain gain = PLLGain.fromSyncCount(mSyncCount); + + //Only update the costas loop if the gain value changes + if(gain != mPLLGain) + { + mPLLGain = gain; + mCostasLoop.setPLLGain(mPLLGain); + } + } + + /** + * Resets the monitor and commands the PLL to use the highest gain level. + */ + public void reset() + { + mSyncCount = 0; + mPLLGain = PLLGain.LEVEL_1; + mCostasLoop.setPLLGain(mPLLGain); + } + + @Override + public void processFrequencyError(long frequencyError) + { + //Only rebroadcast the costas loop frequency error if the gain level has progressed above the + //default gain of 1, meaning that we have received at least 2 sync events + if(mPLLGain != PLLGain.LEVEL_1) + { + mP25Decoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError)); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java index 9c7968ee..3fe40ea5 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -41,10 +41,12 @@ public class CostasLoop implements IPhaseLockedLoop private double mDamping = Math.sqrt(2.0) / 2.0; private double mAlphaGain; private double mBetaGain; - private Tracking mTracking = Tracking.FASTEST; + private PLLGain mPLLGain = PLLGain.LEVEL_1; private double mSymbolRate; private double mSampleRate; - private int mCounter; + private IFrequencyErrorProcessor mFrequencyErrorProcessor; + private long mFrequencyError; + private int mBaudCounter; /** * Costas Loop for tracking and correcting frequency error in a received carrier signal. @@ -60,6 +62,17 @@ public class CostasLoop implements IPhaseLockedLoop updateLoopBandwidth(); } + /** + * Registers the listener to receive the measured frequency error as tracked by the PLL at one + * second intervals. + * + * @param listener to receive frequency error measurement. + */ + public void setFrequencyErrorProcessor(IFrequencyErrorProcessor listener) + { + mFrequencyErrorProcessor = listener; + } + /** * Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of * +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign. @@ -89,21 +102,21 @@ public class CostasLoop implements IPhaseLockedLoop */ private void updateLoopBandwidth() { - double bandwidth = TWO_PI / mTracking.getLoopBandwidth(); + double bandwidth = TWO_PI / mPLLGain.getLoopBandwidth(); mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); } /** - * Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta + * Sets the PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta * gain values in synchronizing with the signal carrier. * - * @param tracking + * @param PLLGain */ - public void setTracking(Tracking tracking) + public void setPLLGain(PLLGain PLLGain) { - mTracking = tracking; + mPLLGain = PLLGain; updateLoopBandwidth(); } @@ -159,7 +172,7 @@ public class CostasLoop implements IPhaseLockedLoop mLoopFrequency += (mBetaGain * phaseError); mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError); - /* Normalize phase between +/- 2 * PI */ + //Normalize phase between +/- 2 * PI if(mLoopPhase > TWO_PI) { mLoopPhase -= TWO_PI; @@ -170,7 +183,7 @@ public class CostasLoop implements IPhaseLockedLoop mLoopPhase += TWO_PI; } - /* Limit frequency to +/- maximum loop frequency */ + //Limit frequency to +/- maximum loop frequency if(mLoopFrequency > mMaximumLoopFrequency) { mLoopFrequency = mMaximumLoopFrequency; @@ -181,17 +194,25 @@ public class CostasLoop implements IPhaseLockedLoop mLoopFrequency = -mMaximumLoopFrequency; } - //TODO: broadcast channel offset to the tuner to use in auto-adjusting tuner PPM -// mCounter++; -// -// if(mCounter > (5 * mSymbolRate)) -// { -// mCounter = 0; -// mLog.info("Current Costas Loop Frequency: " + mLoopFrequency + " [" + -// (int)(mSampleRate / TWO_PI * mLoopFrequency) + " Hz Offset] sample rate [" + mSampleRate + "]"); -// } + //Broadcast current frequency error measurement once a second to an external listener + mBaudCounter++; + + if(mBaudCounter > mSymbolRate) + { + mBaudCounter = 0; + + mFrequencyError = (long)(mSampleRate / TWO_PI * mLoopFrequency); + + if(mFrequencyErrorProcessor != null) + { + mFrequencyErrorProcessor.processFrequencyError(mFrequencyError); + } + } } + /** + * Resets the PLL internal tracking values + */ @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java similarity index 68% rename from src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java rename to src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java index a4611d20..f891b5b2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java @@ -16,27 +16,14 @@ package io.github.dsheirer.dsp.psk.pll; /** - * Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency + * Interface for passing frequency error measurements. */ -public enum Tracking +public interface IFrequencyErrorProcessor { - FASTEST(50.0), - FAST(100.0), - NORMAL(200.0), - SLOW(400.0); - - private double mLoopBandwidth; - - Tracking(double loopBandwidth) - { - mLoopBandwidth = loopBandwidth; - } - /** - * Loop bandwidth setting + * Provides a frequency error measurement to the listener. + * + * @param error value as measured by the provider */ - public double getLoopBandwidth() - { - return mLoopBandwidth; - } + void processFrequencyError(long error); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java new file mode 100644 index 00000000..482a444e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk.pll; + +/** + * Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency + */ +public enum PLLGain +{ + LEVEL_1(50.0, 0, 1), + LEVEL_2(75.0, 2, 4), + LEVEL_3(100.0, 5, 6), + LEVEL_4(125.0, 7, 8), + LEVEL_5(150.0, 9, 10); + + private double mLoopBandwidth; + private int mRangeStart; + private int mRangeEnd; + + PLLGain(double loopBandwidth, int start, int end) + { + mLoopBandwidth = loopBandwidth; + mRangeStart = start; + mRangeEnd = end; + } + + /** + * Loop bandwidth setting + */ + public double getLoopBandwidth() + { + return mLoopBandwidth; + } + + public int getRangeStart() + { + return mRangeStart; + } + + public int getRangeEnd() + { + return mRangeEnd; + } + + /** + * Look up PLL gain from a running sync detection count. + * + * @param syncCount 0-10 + * @return PLL gain appropriate for the sync detection value + */ + public static PLLGain fromSyncCount(int syncCount) + { + if(syncCount <= LEVEL_1.getRangeEnd()) + { + return LEVEL_1; + } + else if(syncCount <= LEVEL_2.getRangeEnd()) + { + return LEVEL_2; + } + else if(syncCount <= LEVEL_3.getRangeEnd()) + { + return LEVEL_3; + } + else if(syncCount <= LEVEL_4.getRangeEnd()) + { + return LEVEL_4; + } + else + { + return LEVEL_5; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java index 5a47798a..139deefb 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java @@ -15,7 +15,13 @@ ******************************************************************************/ package io.github.dsheirer.dsp.symbol; +/** + * Listener interface to be notified each time a sync pattern has been detected and/or when the sync + * has been lost. + */ public interface ISyncDetectListener { - void syncDetected(); + void syncDetected(); + + void syncLost(); } diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java index b762fab1..41f6341c 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java @@ -44,6 +44,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane { mLog.debug("Tower Sync Detected!"); } + + @Override + public void syncLost() + { + //no-op + } }); mDecoder.getTransponderMessageFramer().setSyncDetectListener(new ISyncDetectListener() { @@ -52,6 +58,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane { mLog.debug("Transponder Sync Detected!"); } + + @Override + public void syncLost() + { + //no-op + } }); } diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java index 98807bfa..431a86c0 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java @@ -44,6 +44,12 @@ public class MDC1200Pane extends AbstractAFSK1200Pane { mLog.debug("Sync Detected!"); } + + @Override + public void syncLost() + { + mLog.debug("Sync Lost!"); + } }); } diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java index 83910f71..5ddbf912 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java @@ -44,6 +44,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane { // mLog.debug("Control Sync Detected!"); } + + @Override + public void syncLost() + { + //no-op + } }); mDecoder.getTrafficMessageFramer().setSyncDetectListener(new ISyncDetectListener() { @@ -52,6 +58,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane { // mLog.debug("Traffic Sync Detected!"); } + + @Override + public void syncLost() + { + //no-op + } }); } diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java index 1f96e25e..f136836b 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java @@ -176,7 +176,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane { if(mPLLFrequencyLineChart == null) { - mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40); + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -2500, 2500, 200, 40); } return mPLLFrequencyLineChart; @@ -186,7 +186,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane { if(mSamplesPerSymbolLineChart == null) { - mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40); + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 4.5, 7.0, 0.1, 40); } return mSamplesPerSymbolLineChart; diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java index 9b8b997e..feee0a58 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java @@ -44,6 +44,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane { mLog.debug("ANI Sync Detected!"); } + + @Override + public void syncLost() + { + + } }); mDecoder.getGPSMessageFramer().setSyncDetectListener(new ISyncDetectListener() { @@ -52,6 +58,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane { mLog.debug("GPS Sync Detected!"); } + + @Override + public void syncLost() + { + + } }); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java index 3bf17098..7d5d4462 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java @@ -149,7 +149,7 @@ public class C4FMSymbolFilter implements Listener, ISourceEventL private static final int SAMPLE_RATE = 48000; private static final int SYMBOL_RATE = 4800; - /* Tracking loop gain constant */ + /* PLLGain loop gain constant */ private static final double K_SYMBOL_SPREAD = 0.0100; /* Constraints on symbol spreading */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java index f88348cf..0fe9f4a1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java @@ -132,7 +132,7 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener * issue frequency correction requests to the channel source. * @param sourceEvent to broadcast */ - protected void broadcast(SourceEvent sourceEvent) + public void broadcast(SourceEvent sourceEvent) { if(mSourceEventListener != null) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index f29512f8..a67eeed5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; @@ -36,10 +36,12 @@ import java.util.Map; public class P25DecoderC4FM extends P25Decoder { private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class); - protected static final float SAMPLE_COUNTER_GAIN = 0.5f; + + protected static final float SAMPLE_COUNTER_GAIN = 0.3f; protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator; protected CostasLoop mCostasLoop; + protected AdaptivePLLGainMonitor mPLLGainMonitor; protected P25MessageFramer mMessageFramer; private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); private Map mBasebandFilters = new HashMap<>(); @@ -54,7 +56,7 @@ public class P25DecoderC4FM extends P25Decoder public P25DecoderC4FM(AliasList aliasList) { super(4800.0, aliasList); - setSampleRate(48000.0); + setSampleRate(25000.0); } public void setSampleRate(double sampleRate) @@ -64,14 +66,16 @@ public class P25DecoderC4FM extends P25Decoder mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter()); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); - mCostasLoop.setTracking(Tracking.FASTEST); + mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this); mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - //Message framer can issue a symbol-inversion correction request to the PLL when detected - mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); + //The Costas Loop receives symbol-inversion correction requests when detected. + //The PLL gain monitor receives sync detect/loss signals from the message framer + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor); mMessageFramer.setListener(getMessageProcessor()); + mMessageFramer.setSampleRate(sampleRate); mQPSKDemodulator.setSymbolListener(mMessageFramer); } @@ -165,6 +169,10 @@ public class P25DecoderC4FM extends P25Decoder mCostasLoop.reset(); setSampleRate(sourceEvent.getValue().doubleValue()); break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //Reset the PLL if/when the tuner PPM changes so that we can re-lock + mCostasLoop.reset(); + break; } } @@ -183,5 +191,6 @@ public class P25DecoderC4FM extends P25Decoder public void reset() { mCostasLoop.reset(); + mPLLGainMonitor.reset(); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java index 835c9769..4f2356dd 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -22,20 +22,17 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; public class P25DecoderLSM extends P25Decoder { - private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class); - protected static final float SAMPLE_COUNTER_GAIN = 0.5f; + protected static final float SAMPLE_COUNTER_GAIN = 0.3f; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter2 mBasebandFilter; @@ -43,6 +40,7 @@ public class P25DecoderLSM extends P25Decoder protected DQPSKGardnerDemodulator mQPSKDemodulator; protected P25MessageFramer mMessageFramer; protected CostasLoop mCostasLoop; + protected AdaptivePLLGainMonitor mPLLGainMonitor; protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; /** @@ -54,11 +52,12 @@ public class P25DecoderLSM extends P25Decoder public P25DecoderLSM(AliasList aliasList) { super(4800.0, aliasList); - setSampleRate(48000.0); + setSampleRate(25000.0); } /** * Sets or changes the channel sample rate + * * @param sampleRate in hertz */ public void setSampleRate(double sampleRate) @@ -68,14 +67,17 @@ public class P25DecoderLSM extends P25Decoder mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter()); mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); - mCostasLoop.setTracking(Tracking.NORMAL); + mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - //Message framer can issue a symbol-inversion correction request to the PLL when detected - mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); + //The Costas Loop receives symbol-inversion correction requests when detected. + //The PLL gain monitor receives sync detect/loss signals from the message framer + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor); mMessageFramer.setListener(getMessageProcessor()); + mMessageFramer.setSampleRate(sampleRate); mQPSKDemodulator.setSymbolListener(mMessageFramer); } @@ -97,13 +99,18 @@ public class P25DecoderLSM extends P25Decoder /** * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * * @param reusableComplexBuffer to filter * @return filtered complex buffer */ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) { + //No additional filtering of the channel is currently needed, since the polyphase channelizer + //provides the filtering. + return reusableComplexBuffer; + //User accounting of the incoming buffer is handled by the filter - return mBasebandFilter.filter(reusableComplexBuffer); +// return mBasebandFilter.filter(reusableComplexBuffer); } /** @@ -119,6 +126,10 @@ public class P25DecoderLSM extends P25Decoder setSampleRate(sourceEvent.getValue().doubleValue()); mCostasLoop.reset(); break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //Reset the PLL if/when the tuner PPM changes so that we can re-lock + mCostasLoop.reset(); + break; } } @@ -129,6 +140,7 @@ public class P25DecoderLSM extends P25Decoder public void reset() { mCostasLoop.reset(); + mPLLGainMonitor.reset(); } public void dispose() @@ -163,7 +175,7 @@ public class P25DecoderLSM extends P25Decoder if(filter == null) { - filter = FilterFactory.getLowPass(48000, 7250, 8000, 60, + filter = FilterFactory.getLowPass(getSampleRate(), 7250, 8000, 60, WindowType.HANN, true); mBasebandFilters.put(getSampleRate(), filter); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java index 7419861d..4f6579b1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java @@ -36,11 +36,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.List; public class P25MessageFramer implements Listener { - private final static Logger mLog = - LoggerFactory.getLogger(P25MessageFramer.class); + private final static Logger mLog = LoggerFactory.getLogger(P25MessageFramer.class); /* Determines the threshold for sync pattern soft matching */ private static final int SYNC_MATCH_THRESHOLD = 2; @@ -49,9 +49,10 @@ public class P25MessageFramer implements Listener /* Costas Loop phase lock error correction values. A phase lock error of * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An * error of 180 degrees requires a correction of 1/2 of the symbol rate */ - public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d; - public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d; - public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d; + public static final double DEFAULT_SAMPLE_RATE = 25000.0; + public static final double FREQUENCY_PHASE_CORRECTION_90_DEGREES = 1200.0; + public static final double FREQUENCY_PHASE_CORRECTION_180_DEGREES = 2400.0; + public static final int TSBK_BEGIN = 64; public static final int TSBK_CRC_START = 144; public static final int TSBK_END = 260; @@ -71,9 +72,12 @@ public class P25MessageFramer implements Listener private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector( FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD); - private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48); - private ArrayList mAssemblers = - new ArrayList(); + private PLLPhaseInversionDetector mInversionDetector90CW; + private PLLPhaseInversionDetector mInversionDetector90CCW; + private PLLPhaseInversionDetector mInversionDetector180; + + private MultiSyncPatternMatcher mMatcher; + private List mAssemblers = new ArrayList(); private Listener mListener; private AliasList mAliasList; @@ -88,10 +92,13 @@ public class P25MessageFramer implements Listener * to the message length, and then broadcast that bit buffer to the registered * listener. */ - public P25MessageFramer(AliasList aliasList) + public P25MessageFramer(AliasList aliasList, ISyncDetectListener syncDetectListener) { mAliasList = aliasList; + //Assign the sync detect listener to the matcher with a sync loss threshold equal to the longest message length + mMatcher = new MultiSyncPatternMatcher(syncDetectListener, DataUnitID.LDU1.getMessageLength(), 48); + mPrimarySyncDetector.setListener(new ISyncDetectListener() { @Override @@ -106,6 +113,12 @@ public class P25MessageFramer implements Listener } } } + + @Override + public void syncLost() + { + //no-op + } }); mMatcher.add(mPrimarySyncDetector); @@ -122,25 +135,41 @@ public class P25MessageFramer implements Listener mAssemblers.add(new P25MessageAssembler()); } - public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop) + public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop, ISyncDetectListener syncDetectListener) { - this(aliasList); + this(aliasList, syncDetectListener); if(phaseLockedLoop != null) { //Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern //detections so that we can apply correction to the phase locked loop - mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW, - phaseLockedLoop, PHASE_CORRECTION_90_DEGREES)); + mInversionDetector90CW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW, + phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES); + mMatcher.add(mInversionDetector90CW); - mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW, - phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES)); + mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW, + phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES); + mMatcher.add(mInversionDetector90CCW); - mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180, - phaseLockedLoop, PHASE_CORRECTION_180_DEGREES)); + mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180, + phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_180_DEGREES); + mMatcher.add(mInversionDetector180); } } + /** + * Updates the incoming sample stream sample rate to allow the PLL phase inversion detectors to + * recalculate their internal phase correction values. + * + * @param sampleRate of the incoming sample stream + */ + public void setSampleRate(double sampleRate) + { + mInversionDetector180.setSampleRate(sampleRate); + mInversionDetector90CW.setSampleRate(sampleRate); + mInversionDetector90CCW.setSampleRate(sampleRate); + } + public void dispose() { for(P25MessageAssembler a : mAssemblers) @@ -671,34 +700,46 @@ public class P25MessageFramer implements Listener } /** - * Sync pattern detector to listen for costas loop phase lock errors and - * apply a phase correction to the costas loop so that we don't miss any - * messages. + * Sync pattern detector to listen for costas loop phase lock errors and apply a phase correction to the costas + * loop so that we don't miss any messages. * - * When the costas loop locks with a +/- 90 degree or 180 degree phase - * error, the slicer will incorrectly apply the symbol pattern rotated left - * or right by the phase error. However, we can detect these rotated sync - * patterns and apply immediate phase correction so that message processing - * can continue. + * When the costas loop locks with a +/- 90 degree or 180 degree phase error, the slicer will incorrectly apply + * the symbol pattern rotated left or right by the phase error. However, we can detect these rotated sync patterns + * and apply immediate phase correction so that message processing can continue. */ public class PLLPhaseInversionDetector extends SyncDetector { + private FrameSync mFrameSync; private IPhaseLockedLoop mPhaseLockedLoop; - private double mCorrection; + private double mSampleRate; + private double mFrequencyCorrection; + private double mPllCorrection; - public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction) + /** + * Constructs the PLL phase inversion detector. + * + * @param frameSync pattern to monitor for detecting phase inversion errors + * @param phaseLockedLoop to receive phase correction values + * @param sampleRate of the incoming sample stream + * @param frequencyCorrection to apply to the PLL. Examples: + * QPSK +/-90 degree correction: +/-SYMBOL RATE / 4.0 + * QPSK 180 degree correction: SYMBOL RATE / 2.0 + */ + public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double sampleRate, + double frequencyCorrection) { super(frameSync.getSync()); - + mFrameSync = frameSync; mPhaseLockedLoop = phaseLockedLoop; - mCorrection = correction; + mFrequencyCorrection = frequencyCorrection; + setSampleRate(sampleRate); setListener(new ISyncDetectListener() { @Override public void syncDetected() { - mPhaseLockedLoop.correctInversion(mCorrection); + mPhaseLockedLoop.correctInversion(mPllCorrection); /* Since we detected a sync pattern, start a message assembler */ for(P25MessageAssembler assembler : mAssemblers) @@ -710,7 +751,24 @@ public class P25MessageFramer implements Listener } } } + + @Override + public void syncLost() + { + //no-op + } }); + + } + + /** + * Sets or adjusts the sample rate so that the phase inversion correction value can be recalculated. + * @param sampleRate + */ + public void setSampleRate(double sampleRate) + { + mSampleRate = sampleRate; + mPllCorrection = 2.0 * Math.PI * mFrequencyCorrection / mSampleRate; } } } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 405fbb85..9c1179d1 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -31,6 +31,8 @@ public class SourceEvent NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED, NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, + NOTIFICATION_MEASURED_FREQUENCY_ERROR, + NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED, NOTIFICATION_SAMPLE_RATE_CHANGE, NOTIFICATION_STOP_SAMPLE_STREAM, @@ -124,6 +126,14 @@ public class SourceEvent return mSource; } + /** + * Sets the source associated with this event + */ + public void setSource(Source source) + { + mSource = source; + } + /** * Indicates if this event contains an optional source */ @@ -189,6 +199,28 @@ public class SourceEvent return new SourceEvent(Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection); } + /** + * Creates a new frequency error measurement notification event. + * + * @param frequencyError in hertz + */ + public static SourceEvent frequencyErrorMeasurement(long frequencyError) + { + return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR, frequencyError); + } + + /** + * Creates a new frequency error measurement notification event. This event is different from + * the raw frequency error measurement and indicates that the current state of the PLL is + * locked and tracking the signal. + * + * @param frequencyError in hertz + */ + public static SourceEvent frequencyErrorMeasurementSyncLocked(long frequencyError, String eventDescription) + { + return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED, frequencyError, eventDescription); + } + /** * Creates a new sample rate change event * @@ -288,7 +320,7 @@ public class SourceEvent { return "SOURCE EVENT:" + mEvent + " VALUE:" + (mValue != null ? mValue : "(empty)") + - " SOURCE:" + (mSource != null ? mSource.getClass() : "(null)") + + " SOURCE:" + (mSource != null ? mSource.toString() : "(null)") + " DESCRIPTION:" + (mEventDescription != null ? mEventDescription : ""); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java b/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java new file mode 100644 index 00000000..47a98645 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.source.tuner; + +import io.github.dsheirer.source.SourceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Monitors measured frequency error PPM values from the tuner and (when enabled) applies the current + * error measurement to the tuner controller to adjust the tuner's PPM and align the frequency value. + */ +public class FrequencyErrorCorrectionManager +{ + private final static Logger mLog = LoggerFactory.getLogger(FrequencyErrorCorrectionManager.class); + + private static final double FREQUENCY_CORRECTION_ERROR_THRESHOLD = 0.4; + private static final long AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS = 30 * 1000; //30 seconds + private long mObservationPeriodStart; + private double mPPMRequired; + private boolean mEnabled = true; + private TunerController mTunerController; + + /** + * Constructs a frequency error correction manager for the specified tuner controller. + */ + public FrequencyErrorCorrectionManager(TunerController tunerController) + { + mTunerController = tunerController; + } + + /** + * Enables or disables automatic frequency PPM correction. + * @param enabled to true to turn on automatic PPM adjustments. + */ + public void setEnabled(boolean enabled) + { + mEnabled = enabled; + } + + /** + * Resets monitoring + */ + public void reset() + { + mObservationPeriodStart = 0; + mPPMRequired = 0; + } + + /** + * Applies a PPM adjustment value to the tuner's current PPM setting, when auto-correction is enabled. + */ + private void applyCorrection() + { + if(mEnabled && mTunerController.hasMeasuredFrequencyError()) + { + double frequencyCorrection = mTunerController.getFrequencyCorrection(); + frequencyCorrection -= mTunerController.getPPMFrequencyError(); + + try + { + mLog.info("Auto-Correcting Tuner PPM to [" + frequencyCorrection + "]"); + + mTunerController.setFrequencyCorrection(frequencyCorrection); + reset(); + } + catch(SourceException se) + { + mLog.error("Error while applying automatic frequency error PPM correction value: " + frequencyCorrection); + } + } + } + + /** + * Updates the monitor with a new PPM frequency error measurement from the tuner. + */ + public void updatePPM(double ppm) + { + if(ppm > FREQUENCY_CORRECTION_ERROR_THRESHOLD) + { + if(mObservationPeriodStart == 0) + { + mObservationPeriodStart = System.currentTimeMillis(); + mPPMRequired = ppm; + } + else + { + if(mPPMRequired > 0) + { + mPPMRequired = ppm; + + if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis()) + { + applyCorrection(); + } + } + else + { + //Opposite polarity ppm value ... restart the timer + mObservationPeriodStart = System.currentTimeMillis(); + mPPMRequired = ppm; + } + } + } + else if(ppm < -FREQUENCY_CORRECTION_ERROR_THRESHOLD) + { + if(mObservationPeriodStart == 0) + { + mObservationPeriodStart = System.currentTimeMillis(); + mPPMRequired = ppm; + } + else + { + if(mPPMRequired < 0) + { + mPPMRequired = ppm; + + if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis()) + { + applyCorrection(); + } + } + else + { + //Opposite polarity ppm value ... restart the timer + mObservationPeriodStart = System.currentTimeMillis(); + mPPMRequired = ppm; + } + } + } + else + { + reset(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index d74f52a8..857db597 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -22,6 +22,8 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.TunerEvent.Event; import io.github.dsheirer.source.tuner.manager.ChannelSourceManager; import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with a @@ -29,9 +31,12 @@ import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager; */ public abstract class Tuner implements ISourceEventProcessor { + private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); + private Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); private ChannelSourceManager mChannelSourceManager; private TunerController mTunerController; + private TunerFrequencyErrorMonitor mTunerFrequencyErrorMonitor; private String mName; /** @@ -49,6 +54,9 @@ public abstract class Tuner implements ISourceEventProcessor mChannelSourceManager = new TunerChannelSourceManager(mTunerController); //Register to receive channel count change notifications mChannelSourceManager.addSourceEventListener(this::process); + + mTunerFrequencyErrorMonitor = new TunerFrequencyErrorMonitor(this); + mTunerFrequencyErrorMonitor.start(); } @Override @@ -60,16 +68,23 @@ public abstract class Tuner implements ISourceEventProcessor broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT)); break; case NOTIFICATION_FREQUENCY_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); + broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY_UPDATED)); + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY_ERROR_UPDATED)); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); + broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE_UPDATED)); break; case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: broadcast(new TunerEvent(Tuner.this, Event.LOCK_STATE_CHANGE)); break; + case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED: + mTunerFrequencyErrorMonitor.receive(event); + break; default: + mLog.debug("Unrecognized Source Event: " + event.toString()); break; } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 88b65f68..4ccfac71 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -41,6 +41,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, private int mMiddleUnusableHalfBandwidth; private double mUsableBandwidthPercentage; private Listener mSourceEventListener; + private int mMeasuredFrequencyError; /** * Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels @@ -242,6 +243,46 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, return (int)(getUsableBandwidth() / 2); } + /** + * Current measured frequency error as received/reported from certain downstream decoders. + * @return measured frequency error in hertz averaged over 1-second intervals + */ + public int getMeasuredFrequencyError() + { + return mMeasuredFrequencyError; + } + + /** + * Indicates if this tuner controller has a non-zero measured frequency error value. + */ + public boolean hasMeasuredFrequencyError() + { + return mMeasuredFrequencyError != 0; + } + + /** + * Calculates the current PPM error value as measured by certain downstream decoders. + * @return + */ + public double getPPMFrequencyError() + { + if(hasMeasuredFrequencyError()) + { + return mMeasuredFrequencyError / ((double)getFrequency() / 1E6d); + } + + return 0.0d; + } + + /** + * Sets the measured frequency error average. + * @param measuredFrequencyError in hertz averaged over a 5 second interval. + */ + public void setMeasuredFrequencyError(int measuredFrequencyError) + { + mMeasuredFrequencyError = measuredFrequencyError; + } + /** * Sets the listener to be notified any time that the tuner changes frequency * or bandwidth/sample rate. diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java b/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java index e4f85cc4..c7e9ca8b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java @@ -23,12 +23,13 @@ public class TunerEvent public enum Event { - ADD, - LOCK_STATE_CHANGE, - REMOVE, CHANNEL_COUNT, - FREQUENCY, - SAMPLE_RATE, + FREQUENCY_UPDATED, + FREQUENCY_ERROR_UPDATED, + LOCK_STATE_CHANGE, + MEASURED_FREQUENCY_ERROR_UPDATED, + SAMPLE_RATE_UPDATED, + CLEAR_MAIN_SPECTRAL_DISPLAY, REQUEST_MAIN_SPECTRAL_DISPLAY, REQUEST_NEW_SPECTRAL_DISPLAY; diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java new file mode 100644 index 00000000..d27c48cd --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ +package io.github.dsheirer.source.tuner; + +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.util.ThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Monitors frequency error measurements received from certain decoders (e.g. P25) and averages those + * measurements over an interval period. Averaged measurement values are applied to the tuner controller + * and broadcast as a TunerEvent for visual display. + */ +public class TunerFrequencyErrorMonitor implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(TunerFrequencyErrorMonitor.class); + private static final int PROCESSING_INTERVAL_SECONDS = 5; + private LinkedTransferQueue mMeasurementsQueue = new LinkedTransferQueue<>(); + private List mProcessingMeasurements = new ArrayList(); + private ScheduledFuture mTimerHandle; + private Tuner mTuner; + + /** + * Constructs a monitor for the tuner argument. + * @param tuner to monitor for frequency error measurements + */ + public TunerFrequencyErrorMonitor(Tuner tuner) + { + mTuner = tuner; + } + + /** + * Primary method for receiving frequency error measurements from downstream decoders. + */ + @Override + public void receive(SourceEvent sourceEvent) + { + if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED) + { + mMeasurementsQueue.add(sourceEvent.getValue().intValue()); + } + } + + /** + * Calculates the average frequency error measurement value from discrete measurements received from + * the decoders over the interval period. + */ + private void process() + { + mMeasurementsQueue.drainTo(mProcessingMeasurements); + + if(mProcessingMeasurements.size() > 0) + { + int sum = 0; + + for(Integer measurement: mProcessingMeasurements) + { + sum += measurement; + } + + //Note: the measurements are the frequency correction being applied to each channel to compensate + //for the error int the channel. So, we report the negated value as the current error measurement + broadcast(-(sum / mProcessingMeasurements.size())); + } + else + { + broadcast(0); + } + + mProcessingMeasurements.clear(); + } + + private void broadcast(int averageError) + { + mTuner.getTunerController().setMeasuredFrequencyError(averageError); + mTuner.broadcast(new TunerEvent(mTuner, TunerEvent.Event.MEASURED_FREQUENCY_ERROR_UPDATED)); + } + + /** + * Starts this monitor processing frequency error measurements received from select channel decoders + * and averaging those values over a 1 second interval for transmission as a tuner event. + */ + public void start() + { + if(mTimerHandle == null) + { + mTimerHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, + PROCESSING_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + } + + /** + * Stops this monitor + */ + public void stop() + { + if(mTimerHandle != null) + { + mTimerHandle.cancel(true); + mTimerHandle = null; + broadcast(0); + } + } + + /** + * Runnable implementation to invoke the process() method when the timer fires. + */ + public class Processor implements Runnable + { + @Override + public void run() + { + process(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index 54004b71..3350bcae 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * 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 + * + ******************************************************************************/ package io.github.dsheirer.source.tuner; import io.github.dsheirer.properties.SystemProperties; @@ -31,17 +46,19 @@ public class TunerModel extends AbstractTableModel implements Listener mTuners = new ArrayList<>(); private List> mTunerEventListeners = new ArrayList<>(); private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000"); private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000"); + private DecimalFormat mFrequencyErrorPPMFormat = new DecimalFormat("0.0"); private TunerConfigurationModel mTunerConfigurationModel; @@ -206,15 +223,21 @@ public class TunerModel extends AbstractTableModel implements Listener 0); break; + case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED: + //Rebroadcast these frequency measurement errors to the tuner and tuner controller + //for display and/or correction + broadcast(sourceEvent); + break; default: mLog.info("Unrecognized source event: " + sourceEvent); break; diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java index bb88c8ef..1c1ad8d4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java @@ -30,6 +30,7 @@ public class R820TTunerConfiguration extends TunerConfiguration private R820TTunerController.R820TVGAGain mVGAGain = R820TTunerController.R820TVGAGain.GAIN_210; private double mFrequencyCorrection = 0.0d; private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ; + private boolean mAutoPPMCorrection = true; /** * Default constructor for JAXB @@ -115,4 +116,23 @@ public class R820TTunerConfiguration extends TunerConfiguration { mSampleRate = sampleRate; } + + /** + * Indicates if automatic correction of PPM from measured frequency error is enabled/disabled. + * @return true if auto-correction is enabled. + */ + @JacksonXmlProperty(isAttribute = true, localName = "auto_ppm_correction_enabled") + public boolean getAutoPPMCorrectionEnabled() + { + return mAutoPPMCorrection; + } + + /** + * Sets the enabled state for auto-correction of PPM from measured frequency error values. + * @param enabled + */ + public void setAutoPPMCorrectionEnabled(boolean enabled) + { + mAutoPPMCorrection = enabled; + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java index b8b3e1e8..d06681f2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java @@ -23,6 +23,7 @@ package io.github.dsheirer.source.tuner.rtl.r820t; import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.FrequencyErrorCorrectionManager; import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController; @@ -33,7 +34,6 @@ import org.usb4java.DeviceDescriptor; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; -import javax.swing.JPanel; import javax.usb.UsbException; import java.nio.ByteBuffer; @@ -70,12 +70,36 @@ public class R820TTunerController extends RTL2832TunerController 0x6C, 0x83, 0x80, 0x00, 0x0F, 0x00, 0xC0, 0x30, 0x48, 0xCC, 0x60, 0x00, 0x54, 0xAE, 0x4A, 0xC0}; - private JPanel mEditor; + public FrequencyErrorCorrectionManager mFrequencyErrorCorrectionManager; public R820TTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException { super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER, USABLE_BANDWIDTH_PERCENT); + + mFrequencyErrorCorrectionManager = new FrequencyErrorCorrectionManager(this); + } + + /** + * Manager for applying automatic frequency error PPM adjustments to the tuner controller based on + * frequency error measurements received from certain downstream decoders (e.g. P25). + * @return manager + */ + public FrequencyErrorCorrectionManager getFrequencyErrorCorrectionManager() + { + return mFrequencyErrorCorrectionManager; + } + + /** + * Overrides updates for measured frequency error so that the updates can also be applied to the + * frequency error correction manager for automatic PPM updating. + * @param measuredFrequencyError in hertz averaged over a 5 second interval. + */ + @Override + public void setMeasuredFrequencyError(int measuredFrequencyError) + { + super.setMeasuredFrequencyError(measuredFrequencyError); + getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError()); } @Override @@ -106,6 +130,8 @@ public class R820TTunerController extends RTL2832TunerController double correction = config.getFrequencyCorrection(); setFrequencyCorrection(correction); + getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled()); + R820TGain masterGain = config.getMasterGain(); setGain(masterGain, true); diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java index fd2f009a..47620750 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JOptionPane; @@ -64,6 +65,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor private JButton mTunerInfo; private JComboBox mComboSampleRate; private JSpinner mFrequencyCorrection; + private JCheckBox mAutoPPMEnabled; private JComboBox mComboMasterGain; private JComboBox mComboMixerGain; private JComboBox mComboLNAGain; @@ -202,6 +204,22 @@ public class R820TTunerEditor extends TunerConfigurationEditor add(new JLabel("PPM:")); add(mFrequencyCorrection); + add(new JLabel("")); //Space filler + add(new JLabel("")); //Space filler + add(new JLabel("")); //Space filler + mAutoPPMEnabled = new JCheckBox("PPM Auto-Correction"); + mAutoPPMEnabled.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + boolean enabled = mAutoPPMEnabled.isSelected(); + mController.getFrequencyErrorCorrectionManager().setEnabled(enabled); + save(); + } + }); + add(mAutoPPMEnabled); + add(new JSeparator(JSeparator.HORIZONTAL), "span,grow"); /** @@ -435,6 +453,11 @@ public class R820TTunerEditor extends TunerConfigurationEditor mFrequencyCorrection.setEnabled(enabled); } + if(mAutoPPMEnabled.isEnabled() != enabled) + { + mAutoPPMEnabled.setEnabled(enabled); + } + updateSampleRateToolTip(); if(mController.isLocked()) @@ -524,6 +547,8 @@ public class R820TTunerEditor extends TunerConfigurationEditor .getModel()).getNumber().doubleValue(); config.setFrequencyCorrection(value); + config.setAutoPPMCorrectionEnabled(mAutoPPMEnabled.isSelected()); + config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem()); R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); @@ -560,6 +585,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor setControlsEnabled(true); mConfigurationName.setText(config.getName()); mFrequencyCorrection.setValue(config.getFrequencyCorrection()); + mAutoPPMEnabled.setSelected(config.getAutoPPMCorrectionEnabled()); mComboSampleRate.setSelectedItem(config.getSampleRate()); mComboMasterGain.setSelectedItem(config.getMasterGain()); mComboMixerGain.setSelectedItem(config.getMixerGain()); diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 8dc9e082..064c6f57 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -217,7 +217,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); ReusableComplexBuffer reusableBuffer = mReusableComplexBufferQueue.getBuffer(samples.length); - reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); + System.arraycopy(samples, 0, reusableBuffer.getSamples(), 0, samples.length); + reusableBuffer.setTimestamp(System.currentTimeMillis()); mListener.receive(reusableBuffer); } } diff --git a/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java b/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java index cdc1f507..7dd15a3d 100644 --- a/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java @@ -455,7 +455,10 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, @Override public void run() { - mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize); + if(mMemoryImageSource != null) + { + mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize); + } } }); }