Compare commits

...

17 Commits

Author SHA1 Message Date
Denny
9176ca610f Updates build to Version 0.4.0 Alpha 2 2018-08-05 07:51:40 -04:00
Denny Sheirer
97b31e4d9b Merge pull request #385 from DSheirer/372-icecast-streaming-reconnect
372 Icecast Streaming Reconnect and Metadata Updates
2018-08-05 07:48:28 -04:00
Denny
a3e5520d3e Resolves #372 - Icecast reconnect following network outage. Updates
the lost connection timeout from default of 60 seconds down to 5 seconds
so that the connection state updates more quickly.  Removed complex
error handling and replaced with a simpler approach to catch an error
and disconnect and then attempt a reconnect.

Added new 'Network Unavailable' error state that detects when the network
connection is unavaialable.

Updated the icecast metadata updater to close the TCP session after
successfully writing metadata to the socket.
2018-08-05 07:44:46 -04:00
Denny
8c456972cc Decreases usable bandwidth of the R820T tuner to 98% so that channels
do not get allocated at the spectral extremes where the signal is
degraded.
2018-08-03 11:02:47 -04:00
Denny
35d81ab31d Decreases buffer size for low sample rates for RTL2832 tuners. 2018-08-03 08:06:00 -04:00
Denny
b90b06e600 Merge remote-tracking branch 'origin/master' 2018-08-03 07:57:58 -04:00
Denny
04144ca677 Adds additional RTL 2832 sample rate options 2018-08-03 07:57:47 -04:00
Denny Sheirer
d3c7a50918 Merge pull request #384 from DSheirer/382-rtl-tuner-sample-rate-options
382 - Additional Sample Rate Options for RTL2832 Tuners
2018-08-03 07:53:01 -04:00
Denny
0fd29fa35a Resolves #382 - Adds additional sample rate options for the RTL2832
tuner and adjusts the low sample rate buffer size down to 16 kb.
2018-08-03 07:50:36 -04:00
Denny Sheirer
c00b173064 Merge pull request #383 from DSheirer/381-rtl-tuner-adaptive-native-buffer-size
381 Adaptive USB Transfer Buffer Sizes for RTL2832 Tuners
2018-08-03 07:29:31 -04:00
Denny
2718367259 Resolves #381. Updates USB transfer processor to use native transfer
buffer sizes that adapt to the selected sample rate.
2018-08-03 07:27:58 -04:00
Denny Sheirer
3f0c13096a Merge pull request #380 from DSheirer/374-rtl-tuner-sample-rate-issue
374 RTL Tuner Sample Rates Not Supported (1.44 & 2.88 MHz)
2018-08-03 07:04:28 -04:00
Denny
71ba4adbd7 Resolves #374
Updates polyphase channel manager to correctly calculate the number of
channels for each sample rate change.
2018-08-03 07:02:58 -04:00
Denny Sheirer
3908e29a2e Merge pull request #376 from DSheirer/373-tuner-ppm-monitoring-and-adjustment
#373 - Tuner PPM Error Monitoring and R820T PPM Auto-Correction
2018-07-29 11:18:47 -04:00
Denny
0c47efea03 Resolves #373 - Adds support for Tuner frequency error PPM monitoring
and auto-correction for R820T tuners.

-Updated TunerModel and TunerViewPanel to add 2 new columns to
display the current tuner's PPM setting and (when available) the
measured frequency error averaged over 5 second intervals provided by
certain decoders (e.g. P25 C4FM and LSM).  Frequency error value from
P25 decoders is provided from the Costas Loop PLL once 2 or more P25
message sync events have been observed in a short period.
-Updated R820T tuner to auto-correct PPM when measured frequency error
exceeds the +/- 0.4PPM threshold for a period of 30 seconds or more.
Feature can be enabled/disabled by user in the tuner editor panel.

Resolved several issues with P25 C4FM/LSM decoders that was impacting
performance related to channel misalignment from tuner PPM offsets.
2018-07-29 10:48:29 -04:00
Denny
5c0d97d942 Resolves issue with array out of bounds exception on shutdown while
removing tuners from the tuner model.
2018-07-20 05:59:00 -04:00
Denny
61ae0d2d83 Resolves issue with saved setting for spectral display panel enabled state 2018-07-20 05:58:23 -04:00
54 changed files with 1457 additions and 626 deletions

View File

@@ -14,6 +14,7 @@
<w>nutall</w>
<w>overflowable</w>
<w>polyphase</w>
<w>qpsk</w>
<w>remez</w>
<w>resampler</w>
<w>samplerate</w>

View File

@@ -15,7 +15,7 @@ repositories {
mavenCentral()
}
version = '0.4.0-alpha.1'
version = '0.4.0-alpha.2'
sourceCompatibility = 1.8
sourceSets {

View File

@@ -245,7 +245,11 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
{
if(mBroadcastState != state)
{
mLog.info("[" + getStreamName() + "] status: " + state);
if(state == BroadcastState.CONNECTED || state == BroadcastState.DISCONNECTED)
{
mLog.info("[" + getStreamName() + "] status: " + state);
}
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));

View File

@@ -70,6 +70,11 @@ public enum BroadcastState
*/
MOUNT_POINT_IN_USE("Mount Point In Use", false),
/**
* Network is unavailable or the server address cannot be resolved
*/
NETWORK_UNAVAILABLE("Network Unavailable", false),
/**
* Server is not known or reachable
*/

View File

@@ -20,9 +20,14 @@ package io.github.dsheirer.audio.broadcast;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.Color;
import java.awt.Component;
public class BroadcastStatusPanel extends JPanel
{
@@ -96,7 +101,8 @@ public class BroadcastStatusPanel extends JPanel
setBackground(table.getBackground());
setForeground(Color.LIGHT_GRAY);
}
else if(state == BroadcastState.INVALID_SETTINGS)
else if(state == BroadcastState.INVALID_SETTINGS ||
state == BroadcastState.NETWORK_UNAVAILABLE)
{
setBackground(Color.YELLOW);
setForeground(table.getForeground());

View File

@@ -22,6 +22,7 @@ import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
@@ -35,7 +36,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.nio.channels.UnresolvedAddressException;
@@ -117,70 +117,64 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
{
mMetadataQueue.offer(getSong(metadata));
if(mUpdating.compareAndSet(false, true))
if(!mUpdating.get() && !mMetadataQueue.isEmpty())
{
String song = mMetadataQueue.poll();
while(song != null)
Runnable runnable = new Runnable()
{
HttpRequest updateRequest = createUpdateRequest(song);
if(updateRequest != null)
@Override
public void run()
{
ThreadPool.SCHEDULED.schedule(new Runnable()
if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true))
{
@Override
public void run()
try
{
try
{
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
if(session != null)
{
session.write(updateRequest);
}
}
catch(UnresolvedAddressException uae)
if(session != null)
{
//Do nothing - the server is temporarily unavailable
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
String song = mMetadataQueue.poll();
if(throwableCause instanceof ConnectException)
while(song != null)
{
//Do nothing, the server is unavailable
}
else if(throwableCause instanceof UnresolvedAddressException)
{
//Do nothing - the server is temporarily unavailable
}
else
{
if(!mStackTraceLoggingSuppressed)
HttpRequest updateRequest = createUpdateRequest(song);
if(updateRequest != null)
{
mLog.error("Error sending metadata update. Future errors will " +
"be suppressed", e);
mStackTraceLoggingSuppressed = true;
session.write(updateRequest);
}
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
session.closeNow();
}
}
}, 0l, TimeUnit.SECONDS);
catch(UnresolvedAddressException | RuntimeIoException ure)
{
//Do nothing - the server is temporarily unavailable
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
if(!mStackTraceLoggingSuppressed)
{
mLog.error("Error sending metadata update. Future errors will be suppressed", e);
mStackTraceLoggingSuppressed = true;
}
}
mUpdating.set(false);
}
}
};
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
mUpdating.set(false);
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
}
}

View File

@@ -27,15 +27,14 @@ import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteTimeoutException;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -46,7 +45,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
private static final int WRITE_TIMEOUT_SECONDS = 5;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
@@ -101,7 +101,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
@@ -127,45 +127,41 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
mStreamingSession = future.getSession();
if(connected)
{
mStreamingSession = future.getSession();
mConnecting.set(false);
return;
}
}
catch(RuntimeIoException rioe)
{
if(rioe.getCause() instanceof SocketException)
{
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
}
}
catch(UnresolvedAddressException uae)
{
//What do you do with this? Nothing.
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
}
catch(InterruptedException ie)
catch(Exception e)
{
//We couldn't connect within the timeout period -- disconnect so that we can try again later
disconnect();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
}
disconnect();
mLog.error("Error", e);
//Disregard ... we'll disconnect and try again
}
catch(Throwable t)
{
disconnect();
mLog.error("Throwable error caught", t);
}
disconnect();
mConnecting.set(false);
}
};
@@ -188,6 +184,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
}
}
@@ -263,49 +260,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException)
{
IOException ioe = (IOException)cause;
if(ioe.getMessage() != null)
{
String reason = ioe.getMessage();
if(connected())
{
disconnect();
}
else if(reason.startsWith("Connection reset"))
{
disconnect();
}
else if(reason.startsWith("Operation timed out"))
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
}
else if(ioe instanceof WriteTimeoutException)
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
}
}
else
if(!(cause instanceof IOException))
{
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
disconnect();
}
@Override

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.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<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
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<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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;
}
}

View File

@@ -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<CallEvent> mCallEventListener;
private Listener<DecoderStateEvent> mDecoderStateListener;
private Listener<SquelchState> mSquelchStateListener;
private Listener<SourceEvent> 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<SourceEvent> 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<SourceEvent>
@Override
public void setSourceEventListener(Listener<SourceEvent> 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<SourceEvent>
{
@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;
}
}
}

View File

@@ -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;

View File

@@ -314,7 +314,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
case NOTIFICATION_SAMPLE_RATE_CHANGE:
//Update channel calculator immediately so that channels can be allocated
double sampleRate = sourceEvent.getValue().doubleValue();
int channelCount = (int)Math.ceil(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(sampleRate);
mChannelCalculator.setRates(sampleRate, channelCount);
break;
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
@@ -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() + "]");

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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;
}
}

View File

@@ -237,4 +237,11 @@ public class PolyphaseChannelSource extends TunerChannelSource
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}
@Override
public String toString()
{
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
getTunerChannel().getFrequency();
}
}

View File

@@ -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)

View File

@@ -59,6 +59,11 @@ public interface IPolyphaseChannelOutputProcessor
*/
void setPolyphaseChannelIndices(List<Integer> indexes);
/**
* List of current polyphase channel indices for the output processor
*/
int getPolyphaseChannelIndexCount();
/**
* Updates the synthesis filter taps for this output processor
* @param filter

View File

@@ -134,6 +134,12 @@ public class LTRDecoder implements Listener<ReusableBuffer>, 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

View File

@@ -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.
*/

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.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));
}
}
}

View File

@@ -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()
{

View File

@@ -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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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;
}
}
}

View File

@@ -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();
}

View File

@@ -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
}
});
}

View File

@@ -44,6 +44,12 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("Sync Detected!");
}
@Override
public void syncLost()
{
mLog.debug("Sync Lost!");
}
});
}

View File

@@ -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
}
});
}

View File

@@ -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;

View File

@@ -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()
{
}
});
}

View File

@@ -149,7 +149,7 @@ public class C4FMSymbolFilter implements Listener<ReusableBuffer>, 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 */

View File

@@ -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)
{

View File

@@ -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<Double,float[]> 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();
}
}

View File

@@ -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<Double,float[]> 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);

View File

@@ -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<Dibit>
{
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<Dibit>
/* 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<Dibit>
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
private ArrayList<P25MessageAssembler> mAssemblers =
new ArrayList<P25MessageAssembler>();
private PLLPhaseInversionDetector mInversionDetector90CW;
private PLLPhaseInversionDetector mInversionDetector90CCW;
private PLLPhaseInversionDetector mInversionDetector180;
private MultiSyncPatternMatcher mMatcher;
private List<P25MessageAssembler> mAssemblers = new ArrayList<P25MessageAssembler>();
private Listener<Message> mListener;
private AliasList mAliasList;
@@ -88,10 +92,13 @@ public class P25MessageFramer implements Listener<Dibit>
* 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<Dibit>
}
}
}
@Override
public void syncLost()
{
//no-op
}
});
mMatcher.add(mPrimarySyncDetector);
@@ -122,25 +135,41 @@ public class P25MessageFramer implements Listener<Dibit>
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<Dibit>
}
/**
* 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<Dibit>
}
}
}
@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;
}
}
}

View File

@@ -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 : "");
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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();
}
}
}

View File

@@ -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<TunerEvent> 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;
}
}

View File

@@ -41,6 +41,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
private int mMiddleUnusableHalfBandwidth;
private double mUsableBandwidthPercentage;
private Listener<SourceEvent> 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.

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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<SourceEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(TunerFrequencyErrorMonitor.class);
private static final int PROCESSING_INTERVAL_SECONDS = 5;
private LinkedTransferQueue<Integer> mMeasurementsQueue = new LinkedTransferQueue<>();
private List<Integer> 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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.source.tuner;
import io.github.dsheirer.properties.SystemProperties;
@@ -31,17 +46,19 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
public static final int SAMPLE_RATE = 2;
public static final int FREQUENCY = 3;
public static final int CHANNEL_COUNT = 4;
public static final int SPECTRAL_DISPLAY_MAIN = 5;
public static final int SPECTRAL_DISPLAY_NEW = 6;
public static final int FREQUENCY_ERROR = 5;
public static final int MEASURED_FREQUENCY_ERROR = 6;
public static final int SPECTRAL_DISPLAY_NEW = 7;
private static final String MHZ = " MHz";
private static final String[] COLUMNS =
{"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display"};
private static final String[] COLUMNS = {"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Current PPM",
"Measured Error", "Display"};
private List<Tuner> mTuners = new ArrayList<>();
private List<Listener<TunerEvent>> 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<TunerEven
case CHANNEL_COUNT:
fireTableCellUpdated(index, CHANNEL_COUNT);
break;
case FREQUENCY:
case FREQUENCY_UPDATED:
fireTableCellUpdated(index, FREQUENCY);
mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner());
break;
case SAMPLE_RATE:
case FREQUENCY_ERROR_UPDATED:
fireTableCellUpdated(index, FREQUENCY_ERROR);
break;
case MEASURED_FREQUENCY_ERROR_UPDATED:
fireTableCellUpdated(index, MEASURED_FREQUENCY_ERROR);
break;
case SAMPLE_RATE_UPDATED:
fireTableCellUpdated(index, SAMPLE_RATE);
break;
case REQUEST_MAIN_SPECTRAL_DISPLAY:
fireTableCellUpdated(index, SPECTRAL_DISPLAY_MAIN);
fireTableCellUpdated(index, MEASURED_FREQUENCY_ERROR);
default:
break;
}
@@ -275,8 +298,21 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
{
return "0";
}
case SPECTRAL_DISPLAY_MAIN:
return "Main";
case FREQUENCY_ERROR:
double ppm = tuner.getTunerController().getFrequencyCorrection();
return mFrequencyErrorPPMFormat.format(ppm);
case MEASURED_FREQUENCY_ERROR:
if(tuner.getTunerController().hasMeasuredFrequencyError())
{
StringBuilder sb = new StringBuilder();
sb.append(tuner.getTunerController().getMeasuredFrequencyError());
sb.append("Hz (");
sb.append(mFrequencyErrorPPMFormat.format(tuner.getTunerController().getPPMFrequencyError()));
sb.append("ppm)");
return sb.toString();
}
return "";
case SPECTRAL_DISPLAY_NEW:
return "New";
default:

View File

@@ -85,9 +85,12 @@ public class TunerViewPanel extends JPanel
if(!event.getValueIsAdjusting())
{
int row = mTunerTable.getSelectedRow();
int modelRow = mTunerTable.convertRowIndexToModel(row);
mTunerEditor.setItem(mTunerModel.getTuner(modelRow));
if(row >= 0)
{
int modelRow = mTunerTable.convertRowIndexToModel(row);
mTunerEditor.setItem(mTunerModel.getTuner(modelRow));
}
}
}
});
@@ -127,20 +130,7 @@ public class TunerViewPanel extends JPanel
{
int column = mTunerTable.columnAtPoint(e.getPoint());
if(column == TunerModel.SPECTRAL_DISPLAY_MAIN)
{
int tableRow = mTunerTable.rowAtPoint(e.getPoint());
int modelRow = mTunerTable.convertRowIndexToModel(tableRow);
Tuner tuner = mTunerModel.getTuner(modelRow);
if(tuner != null)
{
mTunerModel.broadcast(new TunerEvent(tuner,
Event.REQUEST_MAIN_SPECTRAL_DISPLAY));
}
}
else if(column == TunerModel.SPECTRAL_DISPLAY_NEW)
if(column == TunerModel.SPECTRAL_DISPLAY_NEW)
{
int tableRow = mTunerTable.rowAtPoint(e.getPoint());
int modelRow = mTunerTable.convertRowIndexToModel(tableRow);
@@ -157,9 +147,7 @@ public class TunerViewPanel extends JPanel
});
TableCellRenderer renderer = new LinkCellRenderer();
mTunerTable.getColumnModel().getColumn(5).setCellRenderer(renderer);
mTunerTable.getColumnModel().getColumn(6).setCellRenderer(renderer);
mTunerTable.getColumnModel().getColumn(TunerModel.SPECTRAL_DISPLAY_NEW).setCellRenderer(renderer);
JScrollPane listScroller = new JScrollPane(mTunerTable);
listScroller.setPreferredSize(new Dimension(400, 20));
@@ -188,15 +176,7 @@ public class TunerViewPanel extends JPanel
value, isSelected, hasFocus, row, column);
label.setForeground(Color.BLUE.brighter());
if(column == TunerModel.SPECTRAL_DISPLAY_MAIN)
{
label.setToolTipText("Show this tuner in the main spectral display");
}
else if(column == TunerModel.SPECTRAL_DISPLAY_NEW)
{
label.setToolTipText("Show this tuner in a new spectral display");
}
label.setToolTipText("Show this tuner in a new spectral display");
return label;
}

View File

@@ -181,6 +181,15 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Ignore these raw frequency measurement errors. We're only interested in the measurements
//that occur when the channel state indicates that we're sync-locked.
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast this measurement event so the producer can process it
sourceEvent.setSource(this);
broadcastProducerSourceEvent(sourceEvent);
break;
//Request events from the consumer
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
setChannelFrequencyCorrection(sourceEvent.getValue().longValue());

View File

@@ -434,8 +434,8 @@ public class TunerChannelSourceManager extends ChannelSourceManager
catch(IllegalArgumentException iae)
{
//Center frequency calculation failed
mLog.debug("Couldn't calculate new tuner center frequency for new tuner channel [" +
tunerChannel + "] unable to allocate new tuner channel");
// mLog.debug("Couldn't calculate new tuner center frequency for new tuner channel [" +
// tunerChannel + "] unable to allocate new tuner channel");
}
}
}
@@ -459,6 +459,11 @@ public class TunerChannelSourceManager extends ChannelSourceManager
//when the polyphase manager has channels allocated
mTunerController.setLocked(getTunerChannelCount() > 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;

View File

@@ -51,12 +51,11 @@ public abstract class RTL2832TunerController extends USBTunerController
private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class);
public final static int TWO_TO_22_POWER = 4194304;
public final static int USB_TRANSFER_BUFFER_SIZE = 131072;
public final static int USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE = 131072;
public final static int USB_TRANSFER_BUFFER_SIZE_LOW_SAMPLE_RATE = 8192;
public final static byte USB_INTERFACE = (byte) 0x0;
public final static byte CONTROL_ENDPOINT_IN = (byte) (LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR);
public final static byte CONTROL_ENDPOINT_OUT = (byte) (LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR);
public final static long TIMEOUT_US = 1000000l; //uSeconds
public final static byte REQUEST_ZERO = (byte) 0;
public final static byte EEPROM_ADDRESS = (byte) 0xA0;
@@ -155,7 +154,7 @@ public abstract class RTL2832TunerController extends USBTunerController
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
USB_TRANSFER_BUFFER_SIZE);
USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
}
@Override
@@ -985,6 +984,18 @@ public abstract class RTL2832TunerController extends USBTunerController
mSampleRate = sampleRate;
mFrequencyController.setSampleRate(sampleRate.getRate());
if(mUSBTransferProcessor != null)
{
if(sampleRate.getRate() > 300000)
{
mUSBTransferProcessor.setBufferSize(USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
}
else
{
mUSBTransferProcessor.setBufferSize(USB_TRANSFER_BUFFER_SIZE_LOW_SAMPLE_RATE);
}
}
}
public void setSampleRateFrequencyCorrection(int ppm) throws SourceException
@@ -1220,23 +1231,29 @@ public abstract class RTL2832TunerController extends USBTunerController
public enum SampleRate
{
/* Note: sample rates below 1.0MHz are subject to aliasing */
RATE_0_230MHZ(0x1F40, 230400, "0.230 MHz"),
RATE_0_240MHZ(0x1E00, 240000, "0.240 MHz"),
RATE_0_256MHZ(0x1C20, 256000, "0.256 MHz"),
RATE_0_288MHZ(0x1900, 288000, "0.288 MHz"),
RATE_0_300MHZ(0x1800, 300000, "0.300 MHz"),
RATE_0_960MHZ(0x0780, 960000, "0.960 MHz"),
RATE_1_024MHZ(0x0708, 1024000, "1.024 MHz"),
RATE_1_200MHZ(0x0600, 1200000, "1.200 MHz"),
RATE_1_440MHZ(0x0500, 1440000, "1.440 MHz"),
RATE_1_920MHZ(0x03C0, 2016000, "2.016 MHz"),
RATE_2_304MHZ(0x0320, 2208000, "2.208 MHz"),
RATE_1_600MHZ(0x0480, 1600000, "1.600 MHz"),
RATE_1_800MHZ(0x0480, 1800000, "1.800 MHz"),
RATE_1_920MHZ(0x03C0, 1920000, "1.920 MHz"),
RATE_2_048MHZ(0x0384, 2048000, "2.048 MHz"),
RATE_2_304MHZ(0x0320, 2304000, "2.304 MHz"),
RATE_2_400MHZ(0x0300, 2400000, "2.400 MHz"),
RATE_2_560MHZ(0x02D0, 2560000, "2.560 MHz"),
RATE_2_880MHZ(0x0280, 2880000, "2.880 MHz");
private int mRatioHigh;
private int mRate;
private String mLabel;
private SampleRate(int ratioHigh,
int rate,
String label)
private SampleRate(int ratioHigh, int rate, String label)
{
mRatioHigh = ratioHigh;
mRate = rate;
@@ -1297,7 +1314,7 @@ public abstract class RTL2832TunerController extends USBTunerController
private int mCheckAddress;
private int mCheckValue;
private TunerTypeCheck(int i2c, int address, int value)
TunerTypeCheck(int i2c, int address, int value)
{
mI2CAddress = i2c;
mCheckAddress = address;

View File

@@ -253,7 +253,7 @@ public class E4KTunerController extends RTL2832TunerController
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
USB_TRANSFER_BUFFER_SIZE);
USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
}
@Override

View File

@@ -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;
}
}

View File

@@ -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;
@@ -43,7 +43,7 @@ public class R820TTunerController extends RTL2832TunerController
public static final long MIN_FREQUENCY = 3180000;
public static final long MAX_FREQUENCY = 1782030000;
public static final double USABLE_BANDWIDTH_PERCENT = 1.0;
public static final double USABLE_BANDWIDTH_PERCENT = 0.98;
public static final int DC_SPIKE_AVOID_BUFFER = 5000;
public static final int R820T_IF_FREQUENCY = 3570000;
@@ -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);

View File

@@ -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<SampleRate> mComboSampleRate;
private JSpinner mFrequencyCorrection;
private JCheckBox mAutoPPMEnabled;
private JComboBox<R820TGain> mComboMasterGain;
private JComboBox<R820TMixerGain> mComboMixerGain;
private JComboBox<R820TLNAGain> 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());

View File

@@ -86,6 +86,32 @@ public class USBTransferProcessor implements TransferCallback
mBufferSize = bufferSize;
}
/**
* Modifies the usb transfer buffer size used for transfering native byte buffers from the
* USB device. Note: changing the buffer size while the transfer processor is running causes
* the transfer to stop momentarily while the existing buffers are destroyed and new buffers
* with the correct size are recreated.
*
* Note: this method is not thread safe. Ensure that no other threads invoke stop() or start()
* while a buffer size change is in progress.
*
* @param bufferSize to use for native usb buffer transfers
*/
public void setBufferSize(int bufferSize)
{
if(bufferSize % 2 == 1)
{
throw new IllegalArgumentException("Buffer size must be a multiple of 2 for complex samples");
}
if(mBufferSize != bufferSize)
{
stop();
mBufferSize = bufferSize;
start();
}
}
/**
* Start USB transfer buffer processing. Subsequent calls to this method after started will be ignored.
*/

View File

@@ -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);
}
}

View File

@@ -15,6 +15,7 @@
******************************************************************************/
package io.github.dsheirer.spectrum;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.source.tuner.Tuner;
import javax.swing.JMenuItem;
@@ -44,6 +45,8 @@ public class ShowTunerMenuItem extends JMenuItem
public void run()
{
mSpectralDisplayPanel.showTuner(mTuner);
SystemProperties properties = SystemProperties.getInstance();
properties.set(SpectralDisplayPanel.SPECTRAL_DISPLAY_ENABLED, true);
}
});
}

View File

@@ -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);
}
}
});
}