mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 17:17:29 -05:00
Compare commits
17 Commits
v0.4.0-alp
...
v0.4.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9176ca610f | ||
|
|
97b31e4d9b | ||
|
|
a3e5520d3e | ||
|
|
8c456972cc | ||
|
|
35d81ab31d | ||
|
|
b90b06e600 | ||
|
|
04144ca677 | ||
|
|
d3c7a50918 | ||
|
|
0fd29fa35a | ||
|
|
c00b173064 | ||
|
|
2718367259 | ||
|
|
3f0c13096a | ||
|
|
71ba4adbd7 | ||
|
|
3908e29a2e | ||
|
|
0c47efea03 | ||
|
|
5c0d97d942 | ||
|
|
61ae0d2d83 |
1
.idea/dictionaries/denny.xml
generated
1
.idea/dictionaries/denny.xml
generated
@@ -14,6 +14,7 @@
|
|||||||
<w>nutall</w>
|
<w>nutall</w>
|
||||||
<w>overflowable</w>
|
<w>overflowable</w>
|
||||||
<w>polyphase</w>
|
<w>polyphase</w>
|
||||||
|
<w>qpsk</w>
|
||||||
<w>remez</w>
|
<w>remez</w>
|
||||||
<w>resampler</w>
|
<w>resampler</w>
|
||||||
<w>samplerate</w>
|
<w>samplerate</w>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
version = '0.4.0-alpha.1'
|
version = '0.4.0-alpha.2'
|
||||||
sourceCompatibility = 1.8
|
sourceCompatibility = 1.8
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|||||||
@@ -245,7 +245,11 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
|
|||||||
{
|
{
|
||||||
if(mBroadcastState != state)
|
if(mBroadcastState != state)
|
||||||
{
|
{
|
||||||
mLog.info("[" + getStreamName() + "] status: " + state);
|
if(state == BroadcastState.CONNECTED || state == BroadcastState.DISCONNECTED)
|
||||||
|
{
|
||||||
|
mLog.info("[" + getStreamName() + "] status: " + state);
|
||||||
|
}
|
||||||
|
|
||||||
mBroadcastState = state;
|
mBroadcastState = state;
|
||||||
|
|
||||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ public enum BroadcastState
|
|||||||
*/
|
*/
|
||||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
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
|
* Server is not known or reachable
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ package io.github.dsheirer.audio.broadcast;
|
|||||||
|
|
||||||
import net.miginfocom.swing.MigLayout;
|
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 javax.swing.table.DefaultTableCellRenderer;
|
||||||
import java.awt.*;
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
|
||||||
public class BroadcastStatusPanel extends JPanel
|
public class BroadcastStatusPanel extends JPanel
|
||||||
{
|
{
|
||||||
@@ -96,7 +101,8 @@ public class BroadcastStatusPanel extends JPanel
|
|||||||
setBackground(table.getBackground());
|
setBackground(table.getBackground());
|
||||||
setForeground(Color.LIGHT_GRAY);
|
setForeground(Color.LIGHT_GRAY);
|
||||||
}
|
}
|
||||||
else if(state == BroadcastState.INVALID_SETTINGS)
|
else if(state == BroadcastState.INVALID_SETTINGS ||
|
||||||
|
state == BroadcastState.NETWORK_UNAVAILABLE)
|
||||||
{
|
{
|
||||||
setBackground(Color.YELLOW);
|
setBackground(Color.YELLOW);
|
||||||
setForeground(table.getForeground());
|
setForeground(table.getForeground());
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
|||||||
import io.github.dsheirer.channel.metadata.Metadata;
|
import io.github.dsheirer.channel.metadata.Metadata;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
import io.github.dsheirer.properties.SystemProperties;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
|
import org.apache.mina.core.RuntimeIoException;
|
||||||
import org.apache.mina.core.future.ConnectFuture;
|
import org.apache.mina.core.future.ConnectFuture;
|
||||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||||
import org.apache.mina.core.session.IoSession;
|
import org.apache.mina.core.session.IoSession;
|
||||||
@@ -35,7 +36,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.channels.UnresolvedAddressException;
|
import java.nio.channels.UnresolvedAddressException;
|
||||||
@@ -117,70 +117,64 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
|||||||
{
|
{
|
||||||
mMetadataQueue.offer(getSong(metadata));
|
mMetadataQueue.offer(getSong(metadata));
|
||||||
|
|
||||||
if(mUpdating.compareAndSet(false, true))
|
if(!mUpdating.get() && !mMetadataQueue.isEmpty())
|
||||||
{
|
{
|
||||||
String song = mMetadataQueue.poll();
|
Runnable runnable = new Runnable()
|
||||||
|
|
||||||
while(song != null)
|
|
||||||
{
|
{
|
||||||
HttpRequest updateRequest = createUpdateRequest(song);
|
@Override
|
||||||
|
public void run()
|
||||||
if(updateRequest != null)
|
|
||||||
{
|
{
|
||||||
ThreadPool.SCHEDULED.schedule(new Runnable()
|
if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true))
|
||||||
{
|
{
|
||||||
@Override
|
try
|
||||||
public void run()
|
|
||||||
{
|
{
|
||||||
try
|
ConnectFuture connectFuture = getSocketConnector()
|
||||||
{
|
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
||||||
ConnectFuture connectFuture = getSocketConnector()
|
mIcecastConfiguration.getPort()));
|
||||||
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
connectFuture.awaitUninterruptibly();
|
||||||
mIcecastConfiguration.getPort()));
|
IoSession session = connectFuture.getSession();
|
||||||
connectFuture.awaitUninterruptibly();
|
|
||||||
IoSession session = connectFuture.getSession();
|
|
||||||
|
|
||||||
if(session != null)
|
if(session != null)
|
||||||
{
|
|
||||||
session.write(updateRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(UnresolvedAddressException uae)
|
|
||||||
{
|
{
|
||||||
//Do nothing - the server is temporarily unavailable
|
String song = mMetadataQueue.poll();
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
Throwable throwableCause = e.getCause();
|
|
||||||
|
|
||||||
if(throwableCause instanceof ConnectException)
|
while(song != null)
|
||||||
{
|
{
|
||||||
//Do nothing, the server is unavailable
|
HttpRequest updateRequest = createUpdateRequest(song);
|
||||||
}
|
|
||||||
else if(throwableCause instanceof UnresolvedAddressException)
|
if(updateRequest != null)
|
||||||
{
|
|
||||||
//Do nothing - the server is temporarily unavailable
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(!mStackTraceLoggingSuppressed)
|
|
||||||
{
|
{
|
||||||
mLog.error("Error sending metadata update. Future errors will " +
|
session.write(updateRequest);
|
||||||
"be suppressed", e);
|
|
||||||
|
|
||||||
mStackTraceLoggingSuppressed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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
|
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||||
song = mMetadataQueue.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
mUpdating.set(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,14 @@ import org.apache.mina.core.RuntimeIoException;
|
|||||||
import org.apache.mina.core.future.ConnectFuture;
|
import org.apache.mina.core.future.ConnectFuture;
|
||||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||||
import org.apache.mina.core.session.IoSession;
|
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.filter.codec.ProtocolCodecFilter;
|
||||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.nio.channels.UnresolvedAddressException;
|
import java.nio.channels.UnresolvedAddressException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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 TERMINATOR = "\r\n";
|
||||||
private final static String SEPARATOR = ":";
|
private final static String SEPARATOR = ":";
|
||||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
|
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 NioSocketConnector mSocketConnector;
|
||||||
private IoSession mStreamingSession = null;
|
private IoSession mStreamingSession = null;
|
||||||
@@ -101,7 +101,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
if(mSocketConnector == null)
|
if(mSocketConnector == null)
|
||||||
{
|
{
|
||||||
mSocketConnector = new NioSocketConnector();
|
mSocketConnector = new NioSocketConnector();
|
||||||
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
|
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||||
@@ -127,45 +127,41 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||||
getBroadcastConfiguration().getPort()));
|
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)
|
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
|
mLog.error("Error", e);
|
||||||
disconnect();
|
//Disregard ... we'll disconnect and try again
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
{
|
{
|
||||||
disconnect();
|
mLog.error("Throwable error caught", t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnect();
|
||||||
mConnecting.set(false);
|
mConnecting.set(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -188,6 +184,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||||
mLastConnectionAttempt = System.currentTimeMillis();
|
mLastConnectionAttempt = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,49 +260,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||||
{
|
{
|
||||||
if(cause instanceof IOException)
|
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
|
|
||||||
{
|
{
|
||||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||||
setBroadcastState(BroadcastState.ERROR);
|
|
||||||
disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
public interface ISyncProcessor
|
public interface ISyncProcessor
|
||||||
{
|
{
|
||||||
public void checkSync( long value );
|
/**
|
||||||
|
* Checks if the value matches a known sync pattern
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean checkSync(long value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,130 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* SDR Trunk
|
* sdr-trunk
|
||||||
* Copyright (C) 2014 Dennis Sheirer
|
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||||
* it under the terms of the GNU General Public License as published by
|
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* later version.
|
||||||
* (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
|
||||||
* This program is distributed in the hope that it will be useful,
|
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
*
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* You should have received a copy of the GNU General Public License along with this program.
|
||||||
* GNU General Public License for more details.
|
* If not, see <http://www.gnu.org/licenses/>
|
||||||
*
|
*
|
||||||
* 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;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.
|
* comparision against the incoming bit stream.
|
||||||
*
|
*
|
||||||
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
|
* 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
|
* 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
|
* of the Long.rotate methods will wrap the MSB around to the LSB and
|
||||||
* corrupt the matching value.
|
* corrupt the matching value.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class MultiSyncPatternMatcher
|
public class MultiSyncPatternMatcher
|
||||||
{
|
{
|
||||||
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
|
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
mBits = Long.rotateLeft( mBits, 1 );
|
private ISyncDetectListener mSyncDetectListener;
|
||||||
|
private int mSyncLossThreshold;
|
||||||
mBits &= mMask;
|
private int mBitCount;
|
||||||
|
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
|
||||||
if( bit2 )
|
|
||||||
{
|
|
||||||
mBits += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for( ISyncProcessor processor: mSyncProcessors )
|
private long mBits = 0;
|
||||||
{
|
private long mMask = 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
for( ISyncProcessor processor: mSyncProcessors )
|
public MultiSyncPatternMatcher(ISyncDetectListener syncDetectListener, int syncLossThreshold, int syncSize)
|
||||||
{
|
{
|
||||||
processor.checkSync( mBits );
|
mSyncDetectListener = syncDetectListener;
|
||||||
}
|
mSyncLossThreshold = syncLossThreshold;
|
||||||
}
|
Validate.isTrue(syncSize < 64);
|
||||||
|
|
||||||
/**
|
//Setup a bit mask of all ones the length of the sync pattern
|
||||||
* Adds a sync processor to receive the bit stream.
|
mMask = (long)((Math.pow(2, syncSize)) - 1);
|
||||||
*/
|
}
|
||||||
public void add( ISyncProcessor processor )
|
|
||||||
{
|
public void dispose()
|
||||||
mSyncProcessors.add( processor );
|
{
|
||||||
}
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
|
|
||||||
public class SoftSyncDetector implements ISyncProcessor
|
public class SoftSyncDetector implements ISyncProcessor
|
||||||
{
|
{
|
||||||
private ISyncDetectListener mListener;
|
private ISyncDetectListener mListener;
|
||||||
private long mPattern;
|
private long mPattern;
|
||||||
private int mThreshold;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public SoftSyncDetector(ISyncDetectListener listener, long pattern, int threshold)
|
||||||
public void checkSync( long value )
|
{
|
||||||
{
|
this(pattern, threshold);
|
||||||
long difference = value ^ mPattern;
|
|
||||||
|
mListener = listener;
|
||||||
if( ( difference == 0 || Long.bitCount( difference ) <= mThreshold ) &&
|
}
|
||||||
mListener != null )
|
|
||||||
{
|
public SoftSyncDetector(long pattern, int threshold)
|
||||||
mListener.syncDetected();
|
{
|
||||||
}
|
mPattern = pattern;
|
||||||
}
|
mThreshold = threshold;
|
||||||
|
}
|
||||||
public void setThreshold( int threshold )
|
|
||||||
{
|
public void dispose()
|
||||||
mThreshold = threshold;
|
{
|
||||||
}
|
mListener = null;
|
||||||
|
}
|
||||||
public void setListener( ISyncDetectListener listener )
|
|
||||||
{
|
@Override
|
||||||
mListener = listener;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
|
|
||||||
public class SyncDetector implements ISyncProcessor
|
public class SyncDetector implements ISyncProcessor
|
||||||
{
|
{
|
||||||
private long mPattern;
|
private long mPattern;
|
||||||
private ISyncDetectListener mListener;
|
private ISyncDetectListener mListener;
|
||||||
|
|
||||||
public SyncDetector( long pattern )
|
public SyncDetector(long pattern)
|
||||||
{
|
{
|
||||||
mPattern = pattern;
|
mPattern = pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncDetector( long pattern, ISyncDetectListener listener )
|
|
||||||
{
|
|
||||||
this( pattern );
|
|
||||||
|
|
||||||
mListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener( ISyncDetectListener listener )
|
|
||||||
{
|
|
||||||
mListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public SyncDetector(long pattern, ISyncDetectListener listener)
|
||||||
public void checkSync( long value )
|
{
|
||||||
{
|
this(pattern);
|
||||||
if( value == mPattern && mListener != null )
|
|
||||||
{
|
mListener = listener;
|
||||||
mListener.syncDetected();
|
}
|
||||||
}
|
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,16 @@ import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
|||||||
import io.github.dsheirer.sample.IOverflowListener;
|
import io.github.dsheirer.sample.IOverflowListener;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.ISourceEventListener;
|
import io.github.dsheirer.source.ISourceEventListener;
|
||||||
|
import io.github.dsheirer.source.ISourceEventProvider;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
|
|
||||||
|
|
||||||
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
|
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
|
||||||
IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider,
|
IDecoderStateEventProvider, ISourceEventListener, ISourceEventProvider, IHeartbeatListener,
|
||||||
IAttributeChangeRequestListener, IOverflowListener
|
ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
|
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<CallEvent> mCallEventListener;
|
||||||
private Listener<DecoderStateEvent> mDecoderStateListener;
|
private Listener<DecoderStateEvent> mDecoderStateListener;
|
||||||
private Listener<SquelchState> mSquelchStateListener;
|
private Listener<SquelchState> mSquelchStateListener;
|
||||||
|
private Listener<SourceEvent> mExternalSourceEventListener;
|
||||||
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
||||||
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
||||||
private ChannelType mChannelType;
|
private ChannelType mChannelType;
|
||||||
private TrafficChannelManager mTrafficChannelEndListener;
|
private TrafficChannelManager mTrafficChannelEndListener;
|
||||||
private CallEvent mTrafficChannelCallEvent;
|
private CallEvent mTrafficChannelCallEvent;
|
||||||
private SourceChangeListener mSourceChangeListener;
|
private SourceEventListener mInternalSourceEventListener;
|
||||||
|
|
||||||
private boolean mSquelchLocked = false;
|
private boolean mSquelchLocked = false;
|
||||||
private boolean mSelected = false;
|
private boolean mSelected = false;
|
||||||
@@ -160,12 +160,12 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
|||||||
@Override
|
@Override
|
||||||
public Listener<SourceEvent> getSourceEventListener()
|
public Listener<SourceEvent> getSourceEventListener()
|
||||||
{
|
{
|
||||||
if(mSourceChangeListener == null)
|
if(mInternalSourceEventListener == null)
|
||||||
{
|
{
|
||||||
mSourceChangeListener = new SourceChangeListener();
|
mInternalSourceEventListener = new SourceEventListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
return mSourceChangeListener;
|
return mInternalSourceEventListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isStandardChannel()
|
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
|
* 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
|
@Override
|
||||||
public void receive(SourceEvent sourceEvent)
|
public void receive(SourceEvent sourceEvent)
|
||||||
{
|
{
|
||||||
if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
|
switch(sourceEvent.getEvent())
|
||||||
{
|
{
|
||||||
long frequency = sourceEvent.getValue().longValue();
|
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||||
|
//Rebroadcast source frequency change events for the decoder(s) to process
|
||||||
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ public enum State
|
|||||||
|
|
||||||
public abstract boolean canChangeTo(State 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()
|
public String getDisplayValue()
|
||||||
{
|
{
|
||||||
return mDisplayValue;
|
return mDisplayValue;
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||||
//Update channel calculator immediately so that channels can be allocated
|
//Update channel calculator immediately so that channels can be allocated
|
||||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||||
int channelCount = (int)Math.ceil(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
|
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(sampleRate);
|
||||||
mChannelCalculator.setRates(sampleRate, channelCount);
|
mChannelCalculator.setRates(sampleRate, channelCount);
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
||||||
@@ -556,6 +556,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||||
//ignore
|
//ignore
|
||||||
break;
|
break;
|
||||||
|
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
|
||||||
|
//Rebroadcast so that the tuner source can process this event
|
||||||
|
mSourceEventBroadcaster.broadcast(sourceEvent);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
||||||
sourceEvent.getEvent() + "]");
|
sourceEvent.getEvent() + "]");
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -237,4 +237,11 @@ public class PolyphaseChannelSource extends TunerChannelSource
|
|||||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
|
||||||
|
getTunerChannel().getFrequency();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
|||||||
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
|
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPolyphaseChannelIndexCount()
|
||||||
|
{
|
||||||
|
return mInputChannelCount;
|
||||||
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
if(mChannelResultsQueue != null)
|
if(mChannelResultsQueue != null)
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ public interface IPolyphaseChannelOutputProcessor
|
|||||||
*/
|
*/
|
||||||
void setPolyphaseChannelIndices(List<Integer> indexes);
|
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
|
* Updates the synthesis filter taps for this output processor
|
||||||
* @param filter
|
* @param filter
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ public class LTRDecoder implements Listener<ReusableBuffer>, ISyncDetectListener
|
|||||||
mSynchronizationMonitor.syncDetected();
|
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
|
* 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
|
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* Synchronization States:
|
* 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
|
* -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.
|
* 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
|
* -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more
|
||||||
* message periods.
|
* message periods.
|
||||||
*/
|
*/
|
||||||
public class SynchronizationMonitor implements ISyncDetectListener
|
public class SynchronizationMonitor implements ISyncDetectListener
|
||||||
{
|
{
|
||||||
@@ -50,6 +50,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the synchronization monitor for the specified message symbol length.
|
* Constructs the synchronization monitor for the specified message symbol length.
|
||||||
|
*
|
||||||
* @param messageLength in symbols.
|
* @param messageLength in symbols.
|
||||||
*/
|
*/
|
||||||
public SynchronizationMonitor(int messageLength)
|
public SynchronizationMonitor(int messageLength)
|
||||||
@@ -127,6 +128,12 @@ public class SynchronizationMonitor implements ISyncDetectListener
|
|||||||
updateSyncState();
|
updateSyncState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments the symbol count for each decoded symbol.
|
* Increments the symbol count for each decoded symbol.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,10 +41,12 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
private double mDamping = Math.sqrt(2.0) / 2.0;
|
private double mDamping = Math.sqrt(2.0) / 2.0;
|
||||||
private double mAlphaGain;
|
private double mAlphaGain;
|
||||||
private double mBetaGain;
|
private double mBetaGain;
|
||||||
private Tracking mTracking = Tracking.FASTEST;
|
private PLLGain mPLLGain = PLLGain.LEVEL_1;
|
||||||
private double mSymbolRate;
|
private double mSymbolRate;
|
||||||
private double mSampleRate;
|
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.
|
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
|
||||||
@@ -60,6 +62,17 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
updateLoopBandwidth();
|
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
|
* 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.
|
* +/- 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()
|
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));
|
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));
|
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.
|
* 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();
|
updateLoopBandwidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +172,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
mLoopFrequency += (mBetaGain * phaseError);
|
mLoopFrequency += (mBetaGain * phaseError);
|
||||||
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
|
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
|
||||||
|
|
||||||
/* Normalize phase between +/- 2 * PI */
|
//Normalize phase between +/- 2 * PI
|
||||||
if(mLoopPhase > TWO_PI)
|
if(mLoopPhase > TWO_PI)
|
||||||
{
|
{
|
||||||
mLoopPhase -= TWO_PI;
|
mLoopPhase -= TWO_PI;
|
||||||
@@ -170,7 +183,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
mLoopPhase += TWO_PI;
|
mLoopPhase += TWO_PI;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Limit frequency to +/- maximum loop frequency */
|
//Limit frequency to +/- maximum loop frequency
|
||||||
if(mLoopFrequency > mMaximumLoopFrequency)
|
if(mLoopFrequency > mMaximumLoopFrequency)
|
||||||
{
|
{
|
||||||
mLoopFrequency = mMaximumLoopFrequency;
|
mLoopFrequency = mMaximumLoopFrequency;
|
||||||
@@ -181,17 +194,25 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
mLoopFrequency = -mMaximumLoopFrequency;
|
mLoopFrequency = -mMaximumLoopFrequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: broadcast channel offset to the tuner to use in auto-adjusting tuner PPM
|
//Broadcast current frequency error measurement once a second to an external listener
|
||||||
// mCounter++;
|
mBaudCounter++;
|
||||||
//
|
|
||||||
// if(mCounter > (5 * mSymbolRate))
|
if(mBaudCounter > mSymbolRate)
|
||||||
// {
|
{
|
||||||
// mCounter = 0;
|
mBaudCounter = 0;
|
||||||
// mLog.info("Current Costas Loop Frequency: " + mLoopFrequency + " [" +
|
|
||||||
// (int)(mSampleRate / TWO_PI * mLoopFrequency) + " Hz Offset] sample rate [" + mSampleRate + "]");
|
mFrequencyError = (long)(mSampleRate / TWO_PI * mLoopFrequency);
|
||||||
// }
|
|
||||||
|
if(mFrequencyErrorProcessor != null)
|
||||||
|
{
|
||||||
|
mFrequencyErrorProcessor.processFrequencyError(mFrequencyError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the PLL internal tracking values
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void reset()
|
public void reset()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,27 +16,14 @@
|
|||||||
package io.github.dsheirer.dsp.psk.pll;
|
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()
|
void processFrequencyError(long error);
|
||||||
{
|
|
||||||
return mLoopBandwidth;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
87
src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java
Normal file
87
src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,13 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.dsp.symbol;
|
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
|
public interface ISyncDetectListener
|
||||||
{
|
{
|
||||||
void syncDetected();
|
void syncDetected();
|
||||||
|
|
||||||
|
void syncLost();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
mLog.debug("Tower Sync Detected!");
|
mLog.debug("Tower Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mDecoder.getTransponderMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
mDecoder.getTransponderMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
||||||
{
|
{
|
||||||
@@ -52,6 +58,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
mLog.debug("Transponder Sync Detected!");
|
mLog.debug("Transponder Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
mLog.debug("Sync Detected!");
|
mLog.debug("Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
mLog.debug("Sync Lost!");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
// mLog.debug("Control Sync Detected!");
|
// mLog.debug("Control Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mDecoder.getTrafficMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
mDecoder.getTrafficMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
||||||
{
|
{
|
||||||
@@ -52,6 +58,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
// mLog.debug("Traffic Sync Detected!");
|
// mLog.debug("Traffic Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
|||||||
{
|
{
|
||||||
if(mPLLFrequencyLineChart == null)
|
if(mPLLFrequencyLineChart == null)
|
||||||
{
|
{
|
||||||
mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
|
mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -2500, 2500, 200, 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mPLLFrequencyLineChart;
|
return mPLLFrequencyLineChart;
|
||||||
@@ -186,7 +186,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
|||||||
{
|
{
|
||||||
if(mSamplesPerSymbolLineChart == null)
|
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;
|
return mSamplesPerSymbolLineChart;
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
mLog.debug("ANI Sync Detected!");
|
mLog.debug("ANI Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mDecoder.getGPSMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
mDecoder.getGPSMessageFramer().setSyncDetectListener(new ISyncDetectListener()
|
||||||
{
|
{
|
||||||
@@ -52,6 +58,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
|||||||
{
|
{
|
||||||
mLog.debug("GPS Sync Detected!");
|
mLog.debug("GPS Sync Detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class C4FMSymbolFilter implements Listener<ReusableBuffer>, ISourceEventL
|
|||||||
private static final int SAMPLE_RATE = 48000;
|
private static final int SAMPLE_RATE = 48000;
|
||||||
private static final int SYMBOL_RATE = 4800;
|
private static final int SYMBOL_RATE = 4800;
|
||||||
|
|
||||||
/* Tracking loop gain constant */
|
/* PLLGain loop gain constant */
|
||||||
private static final double K_SYMBOL_SPREAD = 0.0100;
|
private static final double K_SYMBOL_SPREAD = 0.0100;
|
||||||
|
|
||||||
/* Constraints on symbol spreading */
|
/* Constraints on symbol spreading */
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener
|
|||||||
* issue frequency correction requests to the channel source.
|
* issue frequency correction requests to the channel source.
|
||||||
* @param sourceEvent to broadcast
|
* @param sourceEvent to broadcast
|
||||||
*/
|
*/
|
||||||
protected void broadcast(SourceEvent sourceEvent)
|
public void broadcast(SourceEvent sourceEvent)
|
||||||
{
|
{
|
||||||
if(mSourceEventListener != null)
|
if(mSourceEventListener != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.gain.ComplexFeedForwardGainControl;
|
||||||
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
|
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
|
||||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
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.CostasLoop;
|
||||||
import io.github.dsheirer.dsp.psk.pll.Tracking;
|
|
||||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -36,10 +36,12 @@ import java.util.Map;
|
|||||||
public class P25DecoderC4FM extends P25Decoder
|
public class P25DecoderC4FM extends P25Decoder
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class);
|
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 InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||||
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
|
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
|
||||||
protected CostasLoop mCostasLoop;
|
protected CostasLoop mCostasLoop;
|
||||||
|
protected AdaptivePLLGainMonitor mPLLGainMonitor;
|
||||||
protected P25MessageFramer mMessageFramer;
|
protected P25MessageFramer mMessageFramer;
|
||||||
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
||||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||||
@@ -54,7 +56,7 @@ public class P25DecoderC4FM extends P25Decoder
|
|||||||
public P25DecoderC4FM(AliasList aliasList)
|
public P25DecoderC4FM(AliasList aliasList)
|
||||||
{
|
{
|
||||||
super(4800.0, aliasList);
|
super(4800.0, aliasList);
|
||||||
setSampleRate(48000.0);
|
setSampleRate(25000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSampleRate(double sampleRate)
|
public void setSampleRate(double sampleRate)
|
||||||
@@ -64,14 +66,16 @@ public class P25DecoderC4FM extends P25Decoder
|
|||||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||||
|
|
||||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||||
mCostasLoop.setTracking(Tracking.FASTEST);
|
mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
|
||||||
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||||
|
|
||||||
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
|
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
|
||||||
|
|
||||||
//Message framer can issue a symbol-inversion correction request to the PLL when detected
|
//The Costas Loop receives symbol-inversion correction requests when detected.
|
||||||
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
|
//The PLL gain monitor receives sync detect/loss signals from the message framer
|
||||||
|
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
|
||||||
mMessageFramer.setListener(getMessageProcessor());
|
mMessageFramer.setListener(getMessageProcessor());
|
||||||
|
mMessageFramer.setSampleRate(sampleRate);
|
||||||
mQPSKDemodulator.setSymbolListener(mMessageFramer);
|
mQPSKDemodulator.setSymbolListener(mMessageFramer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +169,10 @@ public class P25DecoderC4FM extends P25Decoder
|
|||||||
mCostasLoop.reset();
|
mCostasLoop.reset();
|
||||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||||
break;
|
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()
|
public void reset()
|
||||||
{
|
{
|
||||||
mCostasLoop.reset();
|
mCostasLoop.reset();
|
||||||
|
mPLLGainMonitor.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.gain.ComplexFeedForwardGainControl;
|
||||||
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
|
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
|
||||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
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.CostasLoop;
|
||||||
import io.github.dsheirer.dsp.psk.pll.Tracking;
|
|
||||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class P25DecoderLSM extends P25Decoder
|
public class P25DecoderLSM extends P25Decoder
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class);
|
protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
|
||||||
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
|
|
||||||
|
|
||||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||||
private ComplexFIRFilter2 mBasebandFilter;
|
private ComplexFIRFilter2 mBasebandFilter;
|
||||||
@@ -43,6 +40,7 @@ public class P25DecoderLSM extends P25Decoder
|
|||||||
protected DQPSKGardnerDemodulator mQPSKDemodulator;
|
protected DQPSKGardnerDemodulator mQPSKDemodulator;
|
||||||
protected P25MessageFramer mMessageFramer;
|
protected P25MessageFramer mMessageFramer;
|
||||||
protected CostasLoop mCostasLoop;
|
protected CostasLoop mCostasLoop;
|
||||||
|
protected AdaptivePLLGainMonitor mPLLGainMonitor;
|
||||||
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,11 +52,12 @@ public class P25DecoderLSM extends P25Decoder
|
|||||||
public P25DecoderLSM(AliasList aliasList)
|
public P25DecoderLSM(AliasList aliasList)
|
||||||
{
|
{
|
||||||
super(4800.0, aliasList);
|
super(4800.0, aliasList);
|
||||||
setSampleRate(48000.0);
|
setSampleRate(25000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets or changes the channel sample rate
|
* Sets or changes the channel sample rate
|
||||||
|
*
|
||||||
* @param sampleRate in hertz
|
* @param sampleRate in hertz
|
||||||
*/
|
*/
|
||||||
public void setSampleRate(double sampleRate)
|
public void setSampleRate(double sampleRate)
|
||||||
@@ -68,14 +67,17 @@ public class P25DecoderLSM extends P25Decoder
|
|||||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||||
|
|
||||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||||
mCostasLoop.setTracking(Tracking.NORMAL);
|
mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
|
||||||
|
|
||||||
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||||
|
|
||||||
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
|
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
|
||||||
|
|
||||||
//Message framer can issue a symbol-inversion correction request to the PLL when detected
|
//The Costas Loop receives symbol-inversion correction requests when detected.
|
||||||
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
|
//The PLL gain monitor receives sync detect/loss signals from the message framer
|
||||||
|
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
|
||||||
mMessageFramer.setListener(getMessageProcessor());
|
mMessageFramer.setListener(getMessageProcessor());
|
||||||
|
mMessageFramer.setSampleRate(sampleRate);
|
||||||
mQPSKDemodulator.setSymbolListener(mMessageFramer);
|
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.
|
* Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
|
||||||
|
*
|
||||||
* @param reusableComplexBuffer to filter
|
* @param reusableComplexBuffer to filter
|
||||||
* @return filtered complex buffer
|
* @return filtered complex buffer
|
||||||
*/
|
*/
|
||||||
protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
|
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
|
//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());
|
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||||
mCostasLoop.reset();
|
mCostasLoop.reset();
|
||||||
break;
|
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()
|
public void reset()
|
||||||
{
|
{
|
||||||
mCostasLoop.reset();
|
mCostasLoop.reset();
|
||||||
|
mPLLGainMonitor.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
@@ -163,7 +175,7 @@ public class P25DecoderLSM extends P25Decoder
|
|||||||
|
|
||||||
if(filter == null)
|
if(filter == null)
|
||||||
{
|
{
|
||||||
filter = FilterFactory.getLowPass(48000, 7250, 8000, 60,
|
filter = FilterFactory.getLowPass(getSampleRate(), 7250, 8000, 60,
|
||||||
WindowType.HANN, true);
|
WindowType.HANN, true);
|
||||||
|
|
||||||
mBasebandFilters.put(getSampleRate(), filter);
|
mBasebandFilters.put(getSampleRate(), filter);
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class P25MessageFramer implements Listener<Dibit>
|
public class P25MessageFramer implements Listener<Dibit>
|
||||||
{
|
{
|
||||||
private final static Logger mLog =
|
private final static Logger mLog = LoggerFactory.getLogger(P25MessageFramer.class);
|
||||||
LoggerFactory.getLogger(P25MessageFramer.class);
|
|
||||||
|
|
||||||
/* Determines the threshold for sync pattern soft matching */
|
/* Determines the threshold for sync pattern soft matching */
|
||||||
private static final int SYNC_MATCH_THRESHOLD = 2;
|
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
|
/* 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
|
* 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 */
|
* 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 DEFAULT_SAMPLE_RATE = 25000.0;
|
||||||
public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
|
public static final double FREQUENCY_PHASE_CORRECTION_90_DEGREES = 1200.0;
|
||||||
public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
|
public static final double FREQUENCY_PHASE_CORRECTION_180_DEGREES = 2400.0;
|
||||||
|
|
||||||
public static final int TSBK_BEGIN = 64;
|
public static final int TSBK_BEGIN = 64;
|
||||||
public static final int TSBK_CRC_START = 144;
|
public static final int TSBK_CRC_START = 144;
|
||||||
public static final int TSBK_END = 260;
|
public static final int TSBK_END = 260;
|
||||||
@@ -71,9 +72,12 @@ public class P25MessageFramer implements Listener<Dibit>
|
|||||||
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
|
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
|
||||||
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
|
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
|
||||||
|
|
||||||
private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
|
private PLLPhaseInversionDetector mInversionDetector90CW;
|
||||||
private ArrayList<P25MessageAssembler> mAssemblers =
|
private PLLPhaseInversionDetector mInversionDetector90CCW;
|
||||||
new ArrayList<P25MessageAssembler>();
|
private PLLPhaseInversionDetector mInversionDetector180;
|
||||||
|
|
||||||
|
private MultiSyncPatternMatcher mMatcher;
|
||||||
|
private List<P25MessageAssembler> mAssemblers = new ArrayList<P25MessageAssembler>();
|
||||||
|
|
||||||
private Listener<Message> mListener;
|
private Listener<Message> mListener;
|
||||||
private AliasList mAliasList;
|
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
|
* to the message length, and then broadcast that bit buffer to the registered
|
||||||
* listener.
|
* listener.
|
||||||
*/
|
*/
|
||||||
public P25MessageFramer(AliasList aliasList)
|
public P25MessageFramer(AliasList aliasList, ISyncDetectListener syncDetectListener)
|
||||||
{
|
{
|
||||||
mAliasList = aliasList;
|
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()
|
mPrimarySyncDetector.setListener(new ISyncDetectListener()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
@@ -106,6 +113,12 @@ public class P25MessageFramer implements Listener<Dibit>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncLost()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mMatcher.add(mPrimarySyncDetector);
|
mMatcher.add(mPrimarySyncDetector);
|
||||||
@@ -122,25 +135,41 @@ public class P25MessageFramer implements Listener<Dibit>
|
|||||||
mAssemblers.add(new P25MessageAssembler());
|
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)
|
if(phaseLockedLoop != null)
|
||||||
{
|
{
|
||||||
//Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern
|
//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
|
//detections so that we can apply correction to the phase locked loop
|
||||||
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
|
mInversionDetector90CW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
|
||||||
phaseLockedLoop, PHASE_CORRECTION_90_DEGREES));
|
phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES);
|
||||||
|
mMatcher.add(mInversionDetector90CW);
|
||||||
|
|
||||||
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
|
mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
|
||||||
phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES));
|
phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES);
|
||||||
|
mMatcher.add(mInversionDetector90CCW);
|
||||||
|
|
||||||
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
|
mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
|
||||||
phaseLockedLoop, PHASE_CORRECTION_180_DEGREES));
|
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()
|
public void dispose()
|
||||||
{
|
{
|
||||||
for(P25MessageAssembler a : mAssemblers)
|
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
|
* Sync pattern detector to listen for costas loop phase lock errors and apply a phase correction to the costas
|
||||||
* apply a phase correction to the costas loop so that we don't miss any
|
* loop so that we don't miss any messages.
|
||||||
* messages.
|
|
||||||
*
|
*
|
||||||
* When the costas loop locks with a +/- 90 degree or 180 degree phase
|
* When the costas loop locks with a +/- 90 degree or 180 degree phase error, the slicer will incorrectly apply
|
||||||
* error, the slicer will incorrectly apply the symbol pattern rotated left
|
* the symbol pattern rotated left or right by the phase error. However, we can detect these rotated sync patterns
|
||||||
* or right by the phase error. However, we can detect these rotated sync
|
* and apply immediate phase correction so that message processing can continue.
|
||||||
* patterns and apply immediate phase correction so that message processing
|
|
||||||
* can continue.
|
|
||||||
*/
|
*/
|
||||||
public class PLLPhaseInversionDetector extends SyncDetector
|
public class PLLPhaseInversionDetector extends SyncDetector
|
||||||
{
|
{
|
||||||
|
private FrameSync mFrameSync;
|
||||||
private IPhaseLockedLoop mPhaseLockedLoop;
|
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());
|
super(frameSync.getSync());
|
||||||
|
mFrameSync = frameSync;
|
||||||
mPhaseLockedLoop = phaseLockedLoop;
|
mPhaseLockedLoop = phaseLockedLoop;
|
||||||
mCorrection = correction;
|
mFrequencyCorrection = frequencyCorrection;
|
||||||
|
setSampleRate(sampleRate);
|
||||||
|
|
||||||
setListener(new ISyncDetectListener()
|
setListener(new ISyncDetectListener()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void syncDetected()
|
public void syncDetected()
|
||||||
{
|
{
|
||||||
mPhaseLockedLoop.correctInversion(mCorrection);
|
mPhaseLockedLoop.correctInversion(mPllCorrection);
|
||||||
|
|
||||||
/* Since we detected a sync pattern, start a message assembler */
|
/* Since we detected a sync pattern, start a message assembler */
|
||||||
for(P25MessageAssembler assembler : mAssemblers)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public class SourceEvent
|
|||||||
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED,
|
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED,
|
||||||
NOTIFICATION_FREQUENCY_CHANGE,
|
NOTIFICATION_FREQUENCY_CHANGE,
|
||||||
NOTIFICATION_FREQUENCY_CORRECTION_CHANGE,
|
NOTIFICATION_FREQUENCY_CORRECTION_CHANGE,
|
||||||
|
NOTIFICATION_MEASURED_FREQUENCY_ERROR,
|
||||||
|
NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED,
|
||||||
NOTIFICATION_SAMPLE_RATE_CHANGE,
|
NOTIFICATION_SAMPLE_RATE_CHANGE,
|
||||||
NOTIFICATION_STOP_SAMPLE_STREAM,
|
NOTIFICATION_STOP_SAMPLE_STREAM,
|
||||||
|
|
||||||
@@ -124,6 +126,14 @@ public class SourceEvent
|
|||||||
return mSource;
|
return mSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the source associated with this event
|
||||||
|
*/
|
||||||
|
public void setSource(Source source)
|
||||||
|
{
|
||||||
|
mSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if this event contains an optional 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);
|
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
|
* Creates a new sample rate change event
|
||||||
*
|
*
|
||||||
@@ -288,7 +320,7 @@ public class SourceEvent
|
|||||||
{
|
{
|
||||||
return "SOURCE EVENT:" + mEvent +
|
return "SOURCE EVENT:" + mEvent +
|
||||||
" VALUE:" + (mValue != null ? mValue : "(empty)") +
|
" VALUE:" + (mValue != null ? mValue : "(empty)") +
|
||||||
" SOURCE:" + (mSource != null ? mSource.getClass() : "(null)") +
|
" SOURCE:" + (mSource != null ? mSource.toString() : "(null)") +
|
||||||
" DESCRIPTION:" + (mEventDescription != null ? mEventDescription : "");
|
" DESCRIPTION:" + (mEventDescription != null ? mEventDescription : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ import io.github.dsheirer.source.SourceEvent;
|
|||||||
import io.github.dsheirer.source.tuner.TunerEvent.Event;
|
import io.github.dsheirer.source.tuner.TunerEvent.Event;
|
||||||
import io.github.dsheirer.source.tuner.manager.ChannelSourceManager;
|
import io.github.dsheirer.source.tuner.manager.ChannelSourceManager;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager;
|
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
|
* 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
|
public abstract class Tuner implements ISourceEventProcessor
|
||||||
{
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(Tuner.class);
|
||||||
|
|
||||||
private Broadcaster<TunerEvent> mTunerEventBroadcaster = new Broadcaster<>();
|
private Broadcaster<TunerEvent> mTunerEventBroadcaster = new Broadcaster<>();
|
||||||
private ChannelSourceManager mChannelSourceManager;
|
private ChannelSourceManager mChannelSourceManager;
|
||||||
private TunerController mTunerController;
|
private TunerController mTunerController;
|
||||||
|
private TunerFrequencyErrorMonitor mTunerFrequencyErrorMonitor;
|
||||||
private String mName;
|
private String mName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +54,9 @@ public abstract class Tuner implements ISourceEventProcessor
|
|||||||
mChannelSourceManager = new TunerChannelSourceManager(mTunerController);
|
mChannelSourceManager = new TunerChannelSourceManager(mTunerController);
|
||||||
//Register to receive channel count change notifications
|
//Register to receive channel count change notifications
|
||||||
mChannelSourceManager.addSourceEventListener(this::process);
|
mChannelSourceManager.addSourceEventListener(this::process);
|
||||||
|
|
||||||
|
mTunerFrequencyErrorMonitor = new TunerFrequencyErrorMonitor(this);
|
||||||
|
mTunerFrequencyErrorMonitor.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,16 +68,23 @@ public abstract class Tuner implements ISourceEventProcessor
|
|||||||
broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT));
|
broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT));
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
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;
|
break;
|
||||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||||
broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE));
|
broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE_UPDATED));
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
||||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
||||||
broadcast(new TunerEvent(Tuner.this, Event.LOCK_STATE_CHANGE));
|
broadcast(new TunerEvent(Tuner.this, Event.LOCK_STATE_CHANGE));
|
||||||
break;
|
break;
|
||||||
|
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
|
||||||
|
mTunerFrequencyErrorMonitor.receive(event);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
mLog.debug("Unrecognized Source Event: " + event.toString());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
|
|||||||
private int mMiddleUnusableHalfBandwidth;
|
private int mMiddleUnusableHalfBandwidth;
|
||||||
private double mUsableBandwidthPercentage;
|
private double mUsableBandwidthPercentage;
|
||||||
private Listener<SourceEvent> mSourceEventListener;
|
private Listener<SourceEvent> mSourceEventListener;
|
||||||
|
private int mMeasuredFrequencyError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels
|
* 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);
|
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
|
* Sets the listener to be notified any time that the tuner changes frequency
|
||||||
* or bandwidth/sample rate.
|
* or bandwidth/sample rate.
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ public class TunerEvent
|
|||||||
|
|
||||||
public enum Event
|
public enum Event
|
||||||
{
|
{
|
||||||
ADD,
|
|
||||||
LOCK_STATE_CHANGE,
|
|
||||||
REMOVE,
|
|
||||||
CHANNEL_COUNT,
|
CHANNEL_COUNT,
|
||||||
FREQUENCY,
|
FREQUENCY_UPDATED,
|
||||||
SAMPLE_RATE,
|
FREQUENCY_ERROR_UPDATED,
|
||||||
|
LOCK_STATE_CHANGE,
|
||||||
|
MEASURED_FREQUENCY_ERROR_UPDATED,
|
||||||
|
SAMPLE_RATE_UPDATED,
|
||||||
|
|
||||||
CLEAR_MAIN_SPECTRAL_DISPLAY,
|
CLEAR_MAIN_SPECTRAL_DISPLAY,
|
||||||
REQUEST_MAIN_SPECTRAL_DISPLAY,
|
REQUEST_MAIN_SPECTRAL_DISPLAY,
|
||||||
REQUEST_NEW_SPECTRAL_DISPLAY;
|
REQUEST_NEW_SPECTRAL_DISPLAY;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
package io.github.dsheirer.source.tuner;
|
||||||
|
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
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 SAMPLE_RATE = 2;
|
||||||
public static final int FREQUENCY = 3;
|
public static final int FREQUENCY = 3;
|
||||||
public static final int CHANNEL_COUNT = 4;
|
public static final int CHANNEL_COUNT = 4;
|
||||||
public static final int SPECTRAL_DISPLAY_MAIN = 5;
|
public static final int FREQUENCY_ERROR = 5;
|
||||||
public static final int SPECTRAL_DISPLAY_NEW = 6;
|
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 MHZ = " MHz";
|
||||||
private static final String[] COLUMNS =
|
private static final String[] COLUMNS = {"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Current PPM",
|
||||||
{"Tuner", "ID", "Sample Rate", "Frequency", "Channels", "Spectral", "Display"};
|
"Measured Error", "Display"};
|
||||||
|
|
||||||
private List<Tuner> mTuners = new ArrayList<>();
|
private List<Tuner> mTuners = new ArrayList<>();
|
||||||
private List<Listener<TunerEvent>> mTunerEventListeners = new ArrayList<>();
|
private List<Listener<TunerEvent>> mTunerEventListeners = new ArrayList<>();
|
||||||
private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000");
|
private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000");
|
||||||
private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000");
|
private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000");
|
||||||
|
private DecimalFormat mFrequencyErrorPPMFormat = new DecimalFormat("0.0");
|
||||||
|
|
||||||
private TunerConfigurationModel mTunerConfigurationModel;
|
private TunerConfigurationModel mTunerConfigurationModel;
|
||||||
|
|
||||||
@@ -206,15 +223,21 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
|||||||
case CHANNEL_COUNT:
|
case CHANNEL_COUNT:
|
||||||
fireTableCellUpdated(index, CHANNEL_COUNT);
|
fireTableCellUpdated(index, CHANNEL_COUNT);
|
||||||
break;
|
break;
|
||||||
case FREQUENCY:
|
case FREQUENCY_UPDATED:
|
||||||
fireTableCellUpdated(index, FREQUENCY);
|
fireTableCellUpdated(index, FREQUENCY);
|
||||||
mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner());
|
mTunerConfigurationModel.tunerFrequencyChanged(event.getTuner());
|
||||||
break;
|
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);
|
fireTableCellUpdated(index, SAMPLE_RATE);
|
||||||
break;
|
break;
|
||||||
case REQUEST_MAIN_SPECTRAL_DISPLAY:
|
case REQUEST_MAIN_SPECTRAL_DISPLAY:
|
||||||
fireTableCellUpdated(index, SPECTRAL_DISPLAY_MAIN);
|
fireTableCellUpdated(index, MEASURED_FREQUENCY_ERROR);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -275,8 +298,21 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
|||||||
{
|
{
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
case SPECTRAL_DISPLAY_MAIN:
|
case FREQUENCY_ERROR:
|
||||||
return "Main";
|
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:
|
case SPECTRAL_DISPLAY_NEW:
|
||||||
return "New";
|
return "New";
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -85,9 +85,12 @@ public class TunerViewPanel extends JPanel
|
|||||||
if(!event.getValueIsAdjusting())
|
if(!event.getValueIsAdjusting())
|
||||||
{
|
{
|
||||||
int row = mTunerTable.getSelectedRow();
|
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());
|
int column = mTunerTable.columnAtPoint(e.getPoint());
|
||||||
|
|
||||||
if(column == TunerModel.SPECTRAL_DISPLAY_MAIN)
|
if(column == TunerModel.SPECTRAL_DISPLAY_NEW)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
int tableRow = mTunerTable.rowAtPoint(e.getPoint());
|
int tableRow = mTunerTable.rowAtPoint(e.getPoint());
|
||||||
int modelRow = mTunerTable.convertRowIndexToModel(tableRow);
|
int modelRow = mTunerTable.convertRowIndexToModel(tableRow);
|
||||||
@@ -157,9 +147,7 @@ public class TunerViewPanel extends JPanel
|
|||||||
});
|
});
|
||||||
|
|
||||||
TableCellRenderer renderer = new LinkCellRenderer();
|
TableCellRenderer renderer = new LinkCellRenderer();
|
||||||
|
mTunerTable.getColumnModel().getColumn(TunerModel.SPECTRAL_DISPLAY_NEW).setCellRenderer(renderer);
|
||||||
mTunerTable.getColumnModel().getColumn(5).setCellRenderer(renderer);
|
|
||||||
mTunerTable.getColumnModel().getColumn(6).setCellRenderer(renderer);
|
|
||||||
|
|
||||||
JScrollPane listScroller = new JScrollPane(mTunerTable);
|
JScrollPane listScroller = new JScrollPane(mTunerTable);
|
||||||
listScroller.setPreferredSize(new Dimension(400, 20));
|
listScroller.setPreferredSize(new Dimension(400, 20));
|
||||||
@@ -188,15 +176,7 @@ public class TunerViewPanel extends JPanel
|
|||||||
value, isSelected, hasFocus, row, column);
|
value, isSelected, hasFocus, row, column);
|
||||||
|
|
||||||
label.setForeground(Color.BLUE.brighter());
|
label.setForeground(Color.BLUE.brighter());
|
||||||
|
label.setToolTipText("Show this tuner in a new spectral display");
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
|
|||||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||||
break;
|
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
|
//Request events from the consumer
|
||||||
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
||||||
setChannelFrequencyCorrection(sourceEvent.getValue().longValue());
|
setChannelFrequencyCorrection(sourceEvent.getValue().longValue());
|
||||||
|
|||||||
@@ -434,8 +434,8 @@ public class TunerChannelSourceManager extends ChannelSourceManager
|
|||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
//Center frequency calculation failed
|
//Center frequency calculation failed
|
||||||
mLog.debug("Couldn't calculate new tuner center frequency for new tuner channel [" +
|
// mLog.debug("Couldn't calculate new tuner center frequency for new tuner channel [" +
|
||||||
tunerChannel + "] unable to allocate 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
|
//when the polyphase manager has channels allocated
|
||||||
mTunerController.setLocked(getTunerChannelCount() > 0);
|
mTunerController.setLocked(getTunerChannelCount() > 0);
|
||||||
break;
|
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:
|
default:
|
||||||
mLog.info("Unrecognized source event: " + sourceEvent);
|
mLog.info("Unrecognized source event: " + sourceEvent);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -51,12 +51,11 @@ public abstract class RTL2832TunerController extends USBTunerController
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class);
|
private final static Logger mLog = LoggerFactory.getLogger(RTL2832TunerController.class);
|
||||||
|
|
||||||
public final static int TWO_TO_22_POWER = 4194304;
|
public final static int TWO_TO_22_POWER = 4194304;
|
||||||
|
public final static int USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE = 131072;
|
||||||
public final static int USB_TRANSFER_BUFFER_SIZE = 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 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_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 byte CONTROL_ENDPOINT_OUT = (byte) (LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR);
|
||||||
|
|
||||||
public final static long TIMEOUT_US = 1000000l; //uSeconds
|
public final static long TIMEOUT_US = 1000000l; //uSeconds
|
||||||
public final static byte REQUEST_ZERO = (byte) 0;
|
public final static byte REQUEST_ZERO = (byte) 0;
|
||||||
public final static byte EEPROM_ADDRESS = (byte) 0xA0;
|
public final static byte EEPROM_ADDRESS = (byte) 0xA0;
|
||||||
@@ -155,7 +154,7 @@ public abstract class RTL2832TunerController extends USBTunerController
|
|||||||
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
|
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
|
||||||
|
|
||||||
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
|
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
|
||||||
USB_TRANSFER_BUFFER_SIZE);
|
USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -985,6 +984,18 @@ public abstract class RTL2832TunerController extends USBTunerController
|
|||||||
mSampleRate = sampleRate;
|
mSampleRate = sampleRate;
|
||||||
|
|
||||||
mFrequencyController.setSampleRate(sampleRate.getRate());
|
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
|
public void setSampleRateFrequencyCorrection(int ppm) throws SourceException
|
||||||
@@ -1220,23 +1231,29 @@ public abstract class RTL2832TunerController extends USBTunerController
|
|||||||
public enum SampleRate
|
public enum SampleRate
|
||||||
{
|
{
|
||||||
/* Note: sample rates below 1.0MHz are subject to aliasing */
|
/* 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_240MHZ(0x1E00, 240000, "0.240 MHz"),
|
||||||
|
RATE_0_256MHZ(0x1C20, 256000, "0.256 MHz"),
|
||||||
RATE_0_288MHZ(0x1900, 288000, "0.288 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_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_200MHZ(0x0600, 1200000, "1.200 MHz"),
|
||||||
RATE_1_440MHZ(0x0500, 1440000, "1.440 MHz"),
|
RATE_1_440MHZ(0x0500, 1440000, "1.440 MHz"),
|
||||||
RATE_1_920MHZ(0x03C0, 2016000, "2.016 MHz"),
|
RATE_1_600MHZ(0x0480, 1600000, "1.600 MHz"),
|
||||||
RATE_2_304MHZ(0x0320, 2208000, "2.208 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_400MHZ(0x0300, 2400000, "2.400 MHz"),
|
||||||
|
RATE_2_560MHZ(0x02D0, 2560000, "2.560 MHz"),
|
||||||
RATE_2_880MHZ(0x0280, 2880000, "2.880 MHz");
|
RATE_2_880MHZ(0x0280, 2880000, "2.880 MHz");
|
||||||
|
|
||||||
private int mRatioHigh;
|
private int mRatioHigh;
|
||||||
private int mRate;
|
private int mRate;
|
||||||
private String mLabel;
|
private String mLabel;
|
||||||
|
|
||||||
private SampleRate(int ratioHigh,
|
private SampleRate(int ratioHigh, int rate, String label)
|
||||||
int rate,
|
|
||||||
String label)
|
|
||||||
{
|
{
|
||||||
mRatioHigh = ratioHigh;
|
mRatioHigh = ratioHigh;
|
||||||
mRate = rate;
|
mRate = rate;
|
||||||
@@ -1297,7 +1314,7 @@ public abstract class RTL2832TunerController extends USBTunerController
|
|||||||
private int mCheckAddress;
|
private int mCheckAddress;
|
||||||
private int mCheckValue;
|
private int mCheckValue;
|
||||||
|
|
||||||
private TunerTypeCheck(int i2c, int address, int value)
|
TunerTypeCheck(int i2c, int address, int value)
|
||||||
{
|
{
|
||||||
mI2CAddress = i2c;
|
mI2CAddress = i2c;
|
||||||
mCheckAddress = address;
|
mCheckAddress = address;
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ public class E4KTunerController extends RTL2832TunerController
|
|||||||
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
|
String deviceName = getTunerType().getLabel() + " " + getUniqueID();
|
||||||
|
|
||||||
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
|
mUSBTransferProcessor = new RTL2832USBTransferProcessor(deviceName, mDeviceHandle, mNativeBufferConverter,
|
||||||
USB_TRANSFER_BUFFER_SIZE);
|
USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class R820TTunerConfiguration extends TunerConfiguration
|
|||||||
private R820TTunerController.R820TVGAGain mVGAGain = R820TTunerController.R820TVGAGain.GAIN_210;
|
private R820TTunerController.R820TVGAGain mVGAGain = R820TTunerController.R820TVGAGain.GAIN_210;
|
||||||
private double mFrequencyCorrection = 0.0d;
|
private double mFrequencyCorrection = 0.0d;
|
||||||
private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ;
|
private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ;
|
||||||
|
private boolean mAutoPPMCorrection = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor for JAXB
|
* Default constructor for JAXB
|
||||||
@@ -115,4 +116,23 @@ public class R820TTunerConfiguration extends TunerConfiguration
|
|||||||
{
|
{
|
||||||
mSampleRate = sampleRate;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
package io.github.dsheirer.source.tuner.rtl.r820t;
|
package io.github.dsheirer.source.tuner.rtl.r820t;
|
||||||
|
|
||||||
import io.github.dsheirer.source.SourceException;
|
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.TunerType;
|
||||||
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
|
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
|
||||||
@@ -33,7 +34,6 @@ import org.usb4java.DeviceDescriptor;
|
|||||||
import org.usb4java.LibUsb;
|
import org.usb4java.LibUsb;
|
||||||
import org.usb4java.LibUsbException;
|
import org.usb4java.LibUsbException;
|
||||||
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.usb.UsbException;
|
import javax.usb.UsbException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ public class R820TTunerController extends RTL2832TunerController
|
|||||||
|
|
||||||
public static final long MIN_FREQUENCY = 3180000;
|
public static final long MIN_FREQUENCY = 3180000;
|
||||||
public static final long MAX_FREQUENCY = 1782030000;
|
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 DC_SPIKE_AVOID_BUFFER = 5000;
|
||||||
|
|
||||||
public static final int R820T_IF_FREQUENCY = 3570000;
|
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,
|
0x6C, 0x83, 0x80, 0x00, 0x0F, 0x00, 0xC0, 0x30,
|
||||||
0x48, 0xCC, 0x60, 0x00, 0x54, 0xAE, 0x4A, 0xC0};
|
0x48, 0xCC, 0x60, 0x00, 0x54, 0xAE, 0x4A, 0xC0};
|
||||||
|
|
||||||
private JPanel mEditor;
|
public FrequencyErrorCorrectionManager mFrequencyErrorCorrectionManager;
|
||||||
|
|
||||||
public R820TTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException
|
public R820TTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException
|
||||||
{
|
{
|
||||||
super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER,
|
super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER,
|
||||||
USABLE_BANDWIDTH_PERCENT);
|
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
|
@Override
|
||||||
@@ -106,6 +130,8 @@ public class R820TTunerController extends RTL2832TunerController
|
|||||||
double correction = config.getFrequencyCorrection();
|
double correction = config.getFrequencyCorrection();
|
||||||
setFrequencyCorrection(correction);
|
setFrequencyCorrection(correction);
|
||||||
|
|
||||||
|
getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled());
|
||||||
|
|
||||||
R820TGain masterGain = config.getMasterGain();
|
R820TGain masterGain = config.getMasterGain();
|
||||||
setGain(masterGain, true);
|
setGain(masterGain, true);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.usb4java.LibUsbException;
|
import org.usb4java.LibUsbException;
|
||||||
|
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
@@ -64,6 +65,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor
|
|||||||
private JButton mTunerInfo;
|
private JButton mTunerInfo;
|
||||||
private JComboBox<SampleRate> mComboSampleRate;
|
private JComboBox<SampleRate> mComboSampleRate;
|
||||||
private JSpinner mFrequencyCorrection;
|
private JSpinner mFrequencyCorrection;
|
||||||
|
private JCheckBox mAutoPPMEnabled;
|
||||||
private JComboBox<R820TGain> mComboMasterGain;
|
private JComboBox<R820TGain> mComboMasterGain;
|
||||||
private JComboBox<R820TMixerGain> mComboMixerGain;
|
private JComboBox<R820TMixerGain> mComboMixerGain;
|
||||||
private JComboBox<R820TLNAGain> mComboLNAGain;
|
private JComboBox<R820TLNAGain> mComboLNAGain;
|
||||||
@@ -202,6 +204,22 @@ public class R820TTunerEditor extends TunerConfigurationEditor
|
|||||||
add(new JLabel("PPM:"));
|
add(new JLabel("PPM:"));
|
||||||
add(mFrequencyCorrection);
|
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");
|
add(new JSeparator(JSeparator.HORIZONTAL), "span,grow");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -435,6 +453,11 @@ public class R820TTunerEditor extends TunerConfigurationEditor
|
|||||||
mFrequencyCorrection.setEnabled(enabled);
|
mFrequencyCorrection.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mAutoPPMEnabled.isEnabled() != enabled)
|
||||||
|
{
|
||||||
|
mAutoPPMEnabled.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
if(mController.isLocked())
|
if(mController.isLocked())
|
||||||
@@ -524,6 +547,8 @@ public class R820TTunerEditor extends TunerConfigurationEditor
|
|||||||
.getModel()).getNumber().doubleValue();
|
.getModel()).getNumber().doubleValue();
|
||||||
config.setFrequencyCorrection(value);
|
config.setFrequencyCorrection(value);
|
||||||
|
|
||||||
|
config.setAutoPPMCorrectionEnabled(mAutoPPMEnabled.isSelected());
|
||||||
|
|
||||||
config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem());
|
config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem());
|
||||||
|
|
||||||
R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem();
|
R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem();
|
||||||
@@ -560,6 +585,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor
|
|||||||
setControlsEnabled(true);
|
setControlsEnabled(true);
|
||||||
mConfigurationName.setText(config.getName());
|
mConfigurationName.setText(config.getName());
|
||||||
mFrequencyCorrection.setValue(config.getFrequencyCorrection());
|
mFrequencyCorrection.setValue(config.getFrequencyCorrection());
|
||||||
|
mAutoPPMEnabled.setSelected(config.getAutoPPMCorrectionEnabled());
|
||||||
mComboSampleRate.setSelectedItem(config.getSampleRate());
|
mComboSampleRate.setSelectedItem(config.getSampleRate());
|
||||||
mComboMasterGain.setSelectedItem(config.getMasterGain());
|
mComboMasterGain.setSelectedItem(config.getMasterGain());
|
||||||
mComboMixerGain.setSelectedItem(config.getMixerGain());
|
mComboMixerGain.setSelectedItem(config.getMixerGain());
|
||||||
|
|||||||
@@ -86,6 +86,32 @@ public class USBTransferProcessor implements TransferCallback
|
|||||||
mBufferSize = bufferSize;
|
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.
|
* Start USB transfer buffer processing. Subsequent calls to this method after started will be ignored.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
|
|||||||
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
|
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
|
||||||
|
|
||||||
ReusableComplexBuffer reusableBuffer = mReusableComplexBufferQueue.getBuffer(samples.length);
|
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);
|
mListener.receive(reusableBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.spectrum;
|
package io.github.dsheirer.spectrum;
|
||||||
|
|
||||||
|
import io.github.dsheirer.properties.SystemProperties;
|
||||||
import io.github.dsheirer.source.tuner.Tuner;
|
import io.github.dsheirer.source.tuner.Tuner;
|
||||||
|
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
@@ -44,6 +45,8 @@ public class ShowTunerMenuItem extends JMenuItem
|
|||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
mSpectralDisplayPanel.showTuner(mTuner);
|
mSpectralDisplayPanel.showTuner(mTuner);
|
||||||
|
SystemProperties properties = SystemProperties.getInstance();
|
||||||
|
properties.set(SpectralDisplayPanel.SPECTRAL_DISPLAY_ENABLED, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,7 +455,10 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
|
|||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize);
|
if(mMemoryImageSource != null)
|
||||||
|
{
|
||||||
|
mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user