mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 10:07:26 -05:00
Network tuner support - rtl_tcp (#2471)
* Add network tuner dialog * wip: network tuner configs * wip stash * working tcp solution * fix copyright headers * fix: resolve rebase conflicts from jRTL library changeover - Drop redundant JFrequencyControl digit-sizing fix now that upstream fixes the same issue via setColumns(1) - Mark SUPPORTED_NETWORK_TUNERS final to match the new EnumSet style - Replace removed SampleRate.getClosest() with a local nearest-match helper in NetworkTunerEditor * Add mise config
This commit is contained in:
@@ -57,10 +57,11 @@ public class ByteNativeBuffer extends AbstractNativeBuffer
|
||||
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
|
||||
if(samples.length % FRAGMENT_SIZE != 0)
|
||||
//Ensure we're an even multiple of the fragment size in bytes (2 bytes per I/Q sample). Typically, this
|
||||
//will be 64k or 128k
|
||||
if(samples.length % (FRAGMENT_SIZE * 2) != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Samples byte[] length [" + samples.length + "] must be an even multiple of " + FRAGMENT_SIZE);
|
||||
throw new IllegalArgumentException("Samples byte[] length [" + samples.length + "] must be an even multiple of " + (FRAGMENT_SIZE * 2));
|
||||
}
|
||||
|
||||
mSamples = samples;
|
||||
|
||||
@@ -37,6 +37,7 @@ public enum TunerClass
|
||||
RSP("RSP"),
|
||||
TEST_TUNER("Test"),
|
||||
RECORDING_TUNER("Recording"),
|
||||
NETWORK_TUNER("Network"),
|
||||
UNKNOWN("Unknown" );
|
||||
|
||||
private final String mDescription;
|
||||
|
||||
@@ -60,6 +60,9 @@ import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerController;
|
||||
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerEditor;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerDisabledEditor;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerEditor;
|
||||
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.recording.RecordingTunerEditor;
|
||||
import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner;
|
||||
@@ -380,6 +383,8 @@ public class TunerFactory
|
||||
return new RspDuoTuner2Configuration(uniqueID);
|
||||
case RSP_DX:
|
||||
return new RspDxTunerConfiguration(uniqueID);
|
||||
case RTL_TCP:
|
||||
return new NetworkTunerConfiguration(uniqueID);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized tuner type [" + type.name() + "]");
|
||||
}
|
||||
@@ -448,6 +453,12 @@ public class TunerFactory
|
||||
discoveredTuner.getClass());
|
||||
case RECORDING_TUNER:
|
||||
return new RecordingTunerEditor(userPreferences, tunerManager, discoveredTuner);
|
||||
case NETWORK_TUNER:
|
||||
if(discoveredTuner.hasTuner())
|
||||
{
|
||||
return new NetworkTunerEditor(userPreferences, tunerManager, discoveredTuner);
|
||||
}
|
||||
return new NetworkTunerDisabledEditor(userPreferences, tunerManager, discoveredTuner);
|
||||
case RTL2832:
|
||||
if(discoveredTuner.hasTuner())
|
||||
{
|
||||
|
||||
@@ -50,6 +50,8 @@ public enum TunerType
|
||||
RSP_DUO_2("RSPduo Tuner 2"),
|
||||
RSP_DX("RSPdx"),
|
||||
|
||||
RTL_TCP("rtl-tcp"),
|
||||
|
||||
TEST("Test"),
|
||||
RECORDING("Recording"),
|
||||
UNKNOWN("Unknown");
|
||||
@@ -84,6 +86,11 @@ public enum TunerType
|
||||
*/
|
||||
public static final EnumSet<TunerType> SUPPORTED_SOUND_CARD_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
|
||||
|
||||
/**
|
||||
* Supported network tuner Types
|
||||
*/
|
||||
public static final EnumSet<TunerType> SUPPORTED_NETWORK_TUNERS = EnumSet.of(RTL_TCP);
|
||||
|
||||
/**
|
||||
* Indicates if this tuner is supported as a USB tuner
|
||||
*/
|
||||
@@ -100,6 +107,14 @@ public enum TunerType
|
||||
return SUPPORTED_SOUND_CARD_TUNERS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this tuner type is supported as a network tuner
|
||||
*/
|
||||
public boolean isSupportedNetworkTuner()
|
||||
{
|
||||
return SUPPORTED_NETWORK_TUNERS.contains(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ import io.github.dsheirer.source.tuner.fcd.proplusV2.FCD2TunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.hackrf.HackRFTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.rtl.fc0012.FC0012TunerConfiguration;
|
||||
@@ -62,6 +63,7 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
|
||||
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
|
||||
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),
|
||||
@JsonSubTypes.Type(value = RspTunerConfiguration.class, name = "rspTunerConfiguration"),
|
||||
@JsonSubTypes.Type(value = NetworkTunerConfiguration.class, name = "networkTunerConfiguration"),
|
||||
})
|
||||
@JacksonXmlRootElement(localName = "tuner_configuration")
|
||||
public abstract class TunerConfiguration
|
||||
|
||||
@@ -168,7 +168,8 @@ public class TunerConfigurationManager implements IDiscoveredTunerStatusListener
|
||||
{
|
||||
TunerType tunerType = discoveredTuner.getTuner().getTunerType();
|
||||
|
||||
if(tunerType != TunerType.RECORDING)
|
||||
//Recording and network tuners' getId() doesn't match the configuration's uniqueID scheme
|
||||
if(tunerType != TunerType.RECORDING && tunerType != TunerType.RTL_TCP)
|
||||
{
|
||||
TunerConfiguration tunerConfiguration = getTunerConfiguration(tunerType, discoveredTuner.getId());
|
||||
|
||||
@@ -193,7 +194,8 @@ public class TunerConfigurationManager implements IDiscoveredTunerStatusListener
|
||||
{
|
||||
TunerType tunerType = discoveredTuner.getTuner().getTunerType();
|
||||
|
||||
if(tunerType != TunerType.RECORDING)
|
||||
//See note in tunerStatusUpdated()
|
||||
if(tunerType != TunerType.RECORDING && tunerType != TunerType.RTL_TCP)
|
||||
{
|
||||
TunerConfiguration tunerConfiguration = getTunerConfiguration(tunerType, discoveredTuner.getId());
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.manager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.TunerClass;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTuner;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerConfiguration;
|
||||
|
||||
public class DiscoveredNetworkTuner extends DiscoveredTuner {
|
||||
private static final Logger mLog = LoggerFactory.getLogger(DiscoveredNetworkTuner.class);
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
public DiscoveredNetworkTuner(UserPreferences userPreferences, NetworkTunerConfiguration networkTunerConfiguration) {
|
||||
mUserPreferences = userPreferences;
|
||||
setTunerConfiguration(networkTunerConfiguration);
|
||||
|
||||
// Default all network tuners to be disabled on startup
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
public NetworkTunerConfiguration getNetworkTunerConfiguration() {
|
||||
return (NetworkTunerConfiguration) getTunerConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TunerClass getTunerClass() {
|
||||
return TunerClass.NETWORK_TUNER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "RTL TCP " + getNetworkTunerConfiguration().getHost() + ":" + getNetworkTunerConfiguration().getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (!hasTuner()) {
|
||||
mTuner = new NetworkTuner(mUserPreferences, this, getNetworkTunerConfiguration());
|
||||
|
||||
try {
|
||||
mTuner.start();
|
||||
} catch (SourceException se) {
|
||||
setErrorMessage("Error - " + se.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getId();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationManager;
|
||||
import io.github.dsheirer.source.tuner.network.NetworkTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
|
||||
import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
|
||||
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.DiscoveredRspDuoTuner1;
|
||||
@@ -137,6 +138,7 @@ public class TunerManager implements IDiscoveredTunerStatusListener
|
||||
}
|
||||
|
||||
discoverRecordingTuners();
|
||||
discoverNetworkTuners();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,6 +439,33 @@ public class TunerManager implements IDiscoveredTunerStatusListener
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover network (e.g. rtl_tcp) based tuners
|
||||
*/
|
||||
private void discoverNetworkTuners()
|
||||
{
|
||||
List<TunerConfiguration> tunerConfigurations = getTunerConfigurationManager().getTunerConfigurations(TunerType.RTL_TCP);
|
||||
|
||||
if(tunerConfigurations.size() > 0)
|
||||
{
|
||||
mLog.info("Discovered [" + tunerConfigurations.size() + "] network tuners");
|
||||
}
|
||||
|
||||
for(TunerConfiguration tunerConfiguration: tunerConfigurations)
|
||||
{
|
||||
if(tunerConfiguration instanceof NetworkTunerConfiguration networkTunerConfiguration)
|
||||
{
|
||||
DiscoveredNetworkTuner discoveredNetworkTuner =
|
||||
new DiscoveredNetworkTuner(mUserPreferences, networkTunerConfiguration);
|
||||
|
||||
discoveredNetworkTuner.addTunerStatusListener(this);
|
||||
discoveredNetworkTuner.setEnabled(false);
|
||||
mLog.info("Tuner Added: " + discoveredNetworkTuner);
|
||||
mDiscoveredTunerModel.addDiscoveredTuner(discoveredNetworkTuner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles tuner status change events. Events are sent to the tuner configuration manager so that it can save
|
||||
* configuration updates and events are also monitored to detect when a user changes the tuner state of a tuner
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationManager;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredNetworkTuner;
|
||||
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
|
||||
public class AddNetworkTunerDialog extends JFrame {
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AddNetworkTunerDialog.class);
|
||||
private UserPreferences mUserPreferences;
|
||||
private DiscoveredTunerModel mDiscoveredTunerModel;
|
||||
private TunerConfigurationManager mTunerConfigurationManager;
|
||||
private JLabel mTypeLabel;
|
||||
private JLabel mHostLabel;
|
||||
private JLabel mPortLabel;
|
||||
private JLabel mFrequencyLabel;
|
||||
private JComboBox mTypeComboBox;
|
||||
private JTextField mHostField;
|
||||
private JTextField mPortField;
|
||||
private JTextField mFrequencyField;
|
||||
private JButton mAddButton;
|
||||
private JButton mCancelButton;
|
||||
|
||||
public AddNetworkTunerDialog(UserPreferences userPreferences, DiscoveredTunerModel discoveredTunerModel,
|
||||
TunerConfigurationManager tunerConfigurationManager) {
|
||||
Validate.notNull(userPreferences, "UserPreferences cannot be null");
|
||||
Validate.notNull(discoveredTunerModel, "TunerModel cannot be null");
|
||||
Validate.notNull(tunerConfigurationManager, "TunerConfigurationManager cannot be null");
|
||||
|
||||
mDiscoveredTunerModel = discoveredTunerModel;
|
||||
mUserPreferences = userPreferences;
|
||||
mTunerConfigurationManager = tunerConfigurationManager;
|
||||
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
setTitle("Add Network Tuner");
|
||||
setSize(new Dimension(500, 250));
|
||||
|
||||
JPanel content = new JPanel();
|
||||
content.setLayout(new MigLayout("", "[align right][][]", "[][][][][grow][]"));
|
||||
|
||||
mTypeLabel = new JLabel("Type:");
|
||||
content.add(mTypeLabel);
|
||||
|
||||
String[] options = { "rtl-tcp" };
|
||||
mTypeComboBox = new JComboBox<>(options);
|
||||
content.add(mTypeComboBox, "span2,grow,wrap");
|
||||
|
||||
mHostLabel = new JLabel("Host:");
|
||||
content.add(mHostLabel);
|
||||
|
||||
mHostField = new JTextField("");
|
||||
mHostField.setToolTipText("Enter the hostname or IP address of the network tuner");
|
||||
content.add(mHostField, "span2,grow,wrap");
|
||||
|
||||
mPortLabel = new JLabel("Port:");
|
||||
content.add(mPortLabel);
|
||||
|
||||
mPortField = new JTextField("");
|
||||
mHostField.setToolTipText("Enter the port number of the network tuner");
|
||||
content.add(mPortField, "span2,grow,wrap");
|
||||
|
||||
mFrequencyLabel = new JLabel("Frequency (Hz):");
|
||||
content.add(mFrequencyLabel);
|
||||
|
||||
mFrequencyField = new JTextField("");
|
||||
mFrequencyField.setToolTipText("Enter the center frequency of the network tuner in Hertz (Hz)");
|
||||
content.add(mFrequencyField, "span2,grow,wrap");
|
||||
|
||||
content.add(new JLabel(""), "wrap");
|
||||
content.add(new JLabel(""));
|
||||
|
||||
mAddButton = new JButton("Add");
|
||||
// mAddButton.setEnabled(false);
|
||||
mAddButton.addActionListener(e -> {
|
||||
|
||||
long frequency = getFrequency();
|
||||
|
||||
if (frequency <= 0 || frequency > Integer.MAX_VALUE) {
|
||||
JOptionPane.showMessageDialog(AddNetworkTunerDialog.this,
|
||||
"Please provide a center frequency (1Hz to 2.14 GHz)",
|
||||
"Center Frequency Required",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
mLog.info("Adding network tuner = frequency [" + frequency + "] host [" + getHost() + ":" + getPort() + "]");
|
||||
|
||||
try {
|
||||
NetworkTunerConfiguration config = NetworkTunerConfiguration.create();
|
||||
config.setFrequency(frequency);
|
||||
config.setHost(getHost());
|
||||
config.setPort(getPort());
|
||||
mTunerConfigurationManager.addTunerConfiguration(config);
|
||||
DiscoveredNetworkTuner discoveredNetworkTuner = new DiscoveredNetworkTuner(mUserPreferences, config);
|
||||
mDiscoveredTunerModel.addDiscoveredTuner(discoveredNetworkTuner);
|
||||
} catch (Exception ex) {
|
||||
mLog.error("Error adding network tuner", ex);
|
||||
}
|
||||
|
||||
AddNetworkTunerDialog.this.setVisible(false);
|
||||
});
|
||||
content.add(mAddButton, "grow,push");
|
||||
|
||||
mCancelButton = new JButton("Cancel");
|
||||
mCancelButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
AddNetworkTunerDialog.this.setVisible(false);
|
||||
}
|
||||
});
|
||||
content.add(mCancelButton, "grow,push");
|
||||
|
||||
setContentPane(content);
|
||||
}
|
||||
|
||||
private long getFrequency() {
|
||||
String text = mFrequencyField.getText();
|
||||
|
||||
if (text != null && !text.isEmpty()) {
|
||||
try {
|
||||
return Long.parseLong(text);
|
||||
} catch (Exception e) {
|
||||
// Do nothing, we couldn't parse the frequency value
|
||||
}
|
||||
}
|
||||
|
||||
return 0l;
|
||||
}
|
||||
|
||||
private String getHost() {
|
||||
return mHostField.getText();
|
||||
}
|
||||
|
||||
private short getPort() {
|
||||
String text = mPortField.getText();
|
||||
|
||||
if (text != null && !text.isEmpty()) {
|
||||
try {
|
||||
return Short.parseShort(text);
|
||||
} catch (Exception e) {
|
||||
// Do nothing, we couldn't parse the port value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
||||
import io.github.dsheirer.source.tuner.Tuner;
|
||||
import io.github.dsheirer.source.tuner.TunerClass;
|
||||
import io.github.dsheirer.source.tuner.TunerType;
|
||||
|
||||
public class NetworkTuner extends Tuner {
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NetworkTuner.class);
|
||||
|
||||
public NetworkTuner(UserPreferences userPreferences, ITunerErrorListener tunerErrorListener,
|
||||
NetworkTunerConfiguration config) {
|
||||
super(new NetworkTunerController(tunerErrorListener, config), tunerErrorListener,
|
||||
userPreferences.getTunerPreference().getChannelizerType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferredName() {
|
||||
return "RTL TCP " + getTunerController().getHost() + ":" + getTunerController().getPort();
|
||||
}
|
||||
|
||||
public NetworkTunerController getTunerController() {
|
||||
return (NetworkTunerController) super.getTunerController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueID() {
|
||||
return getPreferredName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TunerClass getTunerClass() {
|
||||
return TunerClass.NETWORK_TUNER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TunerType getTunerType() {
|
||||
return TunerType.RTL_TCP; // TODO: support more than this.
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSampleSize() {
|
||||
// Note: although sample size is 8, we set it to 11 to align with the
|
||||
// actual noise floor.
|
||||
return 11.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximumUSBBitsPerSecond() {
|
||||
// 16 bits per sample * 2.4 MSPS
|
||||
return 38400000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
|
||||
import io.github.dsheirer.source.tuner.TunerType;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||
|
||||
/**
|
||||
* Network tuner configuration for using network connected tuner as a source
|
||||
*/
|
||||
public class NetworkTunerConfiguration extends TunerConfiguration {
|
||||
private static final Logger mLog = LoggerFactory.getLogger(NetworkTunerConfiguration.class);
|
||||
private String mHost;
|
||||
private short mPort;
|
||||
private boolean mBiasTEnabled = false;
|
||||
private int mGain = 0;
|
||||
private boolean mGainAuto = true;
|
||||
private long mSampleRate = 2_048_000;
|
||||
private boolean mAutoReconnect = false;
|
||||
|
||||
/**
|
||||
* Jackson constructor
|
||||
*/
|
||||
public NetworkTunerConfiguration() {
|
||||
super(52000000, 2200000000l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param uniqueId to use for this configuration
|
||||
*/
|
||||
public NetworkTunerConfiguration(String uniqueId) {
|
||||
this();
|
||||
setUniqueID(uniqueId);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public TunerType getTunerType() {
|
||||
return TunerType.RTL_TCP; // TODO: support more than this.
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "host")
|
||||
public String getHost() {
|
||||
return mHost;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
mHost = host;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "port")
|
||||
public short getPort() {
|
||||
return mPort;
|
||||
}
|
||||
|
||||
public void setPort(short port) {
|
||||
mPort = port;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "bias_t_enabled")
|
||||
public boolean isBiasTEnabled() {
|
||||
return mBiasTEnabled;
|
||||
}
|
||||
|
||||
public void setBiasTEnabled(boolean enabled) {
|
||||
mBiasTEnabled = enabled;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "gain_auto")
|
||||
public boolean isGainAuto() {
|
||||
return mGainAuto;
|
||||
}
|
||||
|
||||
public void setGainAuto(boolean gainAuto) {
|
||||
mGainAuto = gainAuto;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "gain")
|
||||
public int getGain() {
|
||||
return mGain;
|
||||
}
|
||||
|
||||
public void setGain(int gain) {
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
|
||||
public long getSampleRate() {
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
public void setSampleRate(long sampleRate) {
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "auto_reconnect")
|
||||
public boolean isAutoReconnect() {
|
||||
return mAutoReconnect;
|
||||
}
|
||||
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
mAutoReconnect = autoReconnect;
|
||||
}
|
||||
|
||||
public static NetworkTunerConfiguration create() {
|
||||
return new NetworkTunerConfiguration("Network " + System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.dsheirer.buffer.ByteNativeBufferFactory;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
||||
import io.github.dsheirer.source.tuner.TunerController;
|
||||
import io.github.dsheirer.source.tuner.TunerType;
|
||||
|
||||
/**
|
||||
* Tuner controller for an rtl_tcp network tuner. Connects to a remote rtl_tcp server over TCP,
|
||||
* performs the rtl_tcp handshake/command protocol and streams unsigned 8-bit interleaved I/Q
|
||||
* samples from the server into the tuner's native buffer pipeline.
|
||||
*/
|
||||
public class NetworkTunerController extends TunerController {
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NetworkTunerController.class);
|
||||
|
||||
public static final int DC_NOISE_BANDWIDTH = 0;
|
||||
public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00;
|
||||
|
||||
private static final int HANDSHAKE_LENGTH = 12;
|
||||
private static final int CONNECT_TIMEOUT_MS = 5000;
|
||||
private static final int READ_BUFFER_SIZE = 65536; //bytes - matches RTL2832's USB transfer buffer size
|
||||
//ByteNativeBuffer's iterator consumes 8192 complex (I/Q) samples per fragment, i.e. 16384 bytes, so buffers
|
||||
//broadcast to it must be an even multiple of 16384 bytes, not just 8192.
|
||||
private static final int FRAGMENT_SIZE = 16384;
|
||||
private static final long[] RECONNECT_DELAYS_MS = {1000, 2000, 5000, 10000};
|
||||
|
||||
//rtl_tcp client command identifiers (see librtlsdr rtl_tcp.c)
|
||||
private static final int CMD_SET_FREQUENCY = 0x01;
|
||||
private static final int CMD_SET_SAMPLE_RATE = 0x02;
|
||||
private static final int CMD_SET_GAIN_MODE = 0x03;
|
||||
private static final int CMD_SET_GAIN = 0x04;
|
||||
private static final int CMD_SET_BIAS_TEE = 0x0E;
|
||||
|
||||
private final String mHost;
|
||||
private final int mPort;
|
||||
private long mCenterFrequency;
|
||||
private long mSampleRate;
|
||||
private boolean mGainAuto;
|
||||
private int mGain;
|
||||
private boolean mBiasTEnabled;
|
||||
private boolean mAutoReconnect;
|
||||
|
||||
private final ByteNativeBufferFactory mNativeBufferFactory = new ByteNativeBufferFactory();
|
||||
private volatile OutputStream mOutputStream;
|
||||
private volatile boolean mRunning;
|
||||
private Thread mConnectionThread;
|
||||
|
||||
public NetworkTunerController(ITunerErrorListener tunerErrorListener, NetworkTunerConfiguration config) {
|
||||
super(tunerErrorListener);
|
||||
mHost = config.getHost();
|
||||
mPort = config.getPort() & 0xFFFF; //port is stored as a signed short - treat as unsigned
|
||||
mCenterFrequency = config.getFrequency();
|
||||
if (mCenterFrequency == 0) {
|
||||
mCenterFrequency = 100000000; // Default to 100 MHz if no frequency is provided
|
||||
}
|
||||
mSampleRate = config.getSampleRate();
|
||||
mGainAuto = config.isGainAuto();
|
||||
mGain = config.getGain();
|
||||
mBiasTEnabled = config.isBiasTEnabled();
|
||||
mAutoReconnect = config.isAutoReconnect();
|
||||
|
||||
setMinimumFrequency(1000000L); // 1 MHz
|
||||
setMaximumFrequency(3000000000L); // 3 GHz
|
||||
setMiddleUnusableHalfBandwidth(DC_NOISE_BANDWIDTH);
|
||||
setUsableBandwidthPercentage(USABLE_BANDWIDTH_PERCENTAGE);
|
||||
mNativeBufferFactory.setSamplesPerMillisecond(mSampleRate / 1000.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws SourceException {
|
||||
if (mRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Broadcasts the sample rate to downstream listeners (e.g. the channel source manager) - without
|
||||
//this, getSampleRate() remains 0 and channelizing never starts.
|
||||
mFrequencyController.setSampleRate((int) mSampleRate);
|
||||
|
||||
mRunning = true;
|
||||
mConnectionThread = new Thread(this::runConnectionLoop, "network-tuner-" + mHost + ":" + mPort);
|
||||
mConnectionThread.setDaemon(true);
|
||||
mConnectionThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
mRunning = false;
|
||||
|
||||
Thread thread = mConnectionThread;
|
||||
mConnectionThread = null;
|
||||
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on a background thread for the lifetime of this controller (until stop() is called).
|
||||
* Connects, streams samples until the connection drops or is closed, and - when auto-reconnect
|
||||
* is enabled - retries with an increasing backoff delay. When auto-reconnect is disabled, a
|
||||
* connection failure is reported as a tuner error and the loop exits.
|
||||
*/
|
||||
private void runConnectionLoop() {
|
||||
int attempt = 0;
|
||||
|
||||
while (mRunning) {
|
||||
try {
|
||||
connectAndStream();
|
||||
attempt = 0;
|
||||
} catch (IOException ioe) {
|
||||
if (!mRunning) {
|
||||
return; //stop() was called - exit quietly
|
||||
}
|
||||
|
||||
if (!mAutoReconnect) {
|
||||
mLog.error("Network tuner [" + mHost + ":" + mPort + "] connection error", ioe);
|
||||
setErrorMessage("Error - " + ioe.getMessage());
|
||||
mRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
long delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
||||
attempt++;
|
||||
mLog.warn("Network tuner [" + mHost + ":" + mPort + "] disconnected - reconnecting in " + delay
|
||||
+ " ms - " + ioe.getMessage());
|
||||
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the rtl_tcp server, performs the handshake and initial command sequence, and then
|
||||
* blocks streaming samples until the connection is closed (by either side) or stop() is called.
|
||||
*/
|
||||
private void connectAndStream() throws IOException {
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(mHost, mPort), CONNECT_TIMEOUT_MS);
|
||||
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
mOutputStream = out;
|
||||
|
||||
readAndValidateHandshake(in);
|
||||
|
||||
sendCommand(out, CMD_SET_SAMPLE_RATE, (int) mSampleRate);
|
||||
sendCommand(out, CMD_SET_FREQUENCY, (int) mCenterFrequency);
|
||||
sendCommand(out, CMD_SET_GAIN_MODE, mGainAuto ? 0 : 1);
|
||||
if (!mGainAuto) {
|
||||
sendCommand(out, CMD_SET_GAIN, mGain);
|
||||
}
|
||||
sendCommand(out, CMD_SET_BIAS_TEE, mBiasTEnabled ? 1 : 0);
|
||||
|
||||
mLog.info("Network tuner connected [" + mHost + ":" + mPort + "]");
|
||||
|
||||
byte[] readBuffer = new byte[READ_BUFFER_SIZE];
|
||||
byte[] pending = new byte[0]; //leftover bytes (< FRAGMENT_SIZE) carried over from the previous read
|
||||
int read;
|
||||
|
||||
while (mRunning && (read = in.read(readBuffer)) != -1) {
|
||||
//Socket reads rarely align to FRAGMENT_SIZE (ByteNativeBuffer requires an even multiple of it),
|
||||
//so accumulate leftover bytes across reads and only broadcast full FRAGMENT_SIZE-aligned chunks.
|
||||
byte[] combined = new byte[pending.length + read];
|
||||
System.arraycopy(pending, 0, combined, 0, pending.length);
|
||||
System.arraycopy(readBuffer, 0, combined, pending.length, read);
|
||||
|
||||
int usableLength = combined.length - (combined.length % FRAGMENT_SIZE);
|
||||
if (usableLength > 0) {
|
||||
ByteBuffer samples = ByteBuffer.wrap(combined, 0, usableLength).slice();
|
||||
broadcast(mNativeBufferFactory.getBuffer(samples, System.currentTimeMillis()));
|
||||
}
|
||||
pending = Arrays.copyOfRange(combined, usableLength, combined.length);
|
||||
}
|
||||
|
||||
if (mRunning) {
|
||||
throw new EOFException("Network tuner connection closed by remote host");
|
||||
}
|
||||
} finally {
|
||||
mOutputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and validates the 12-byte rtl_tcp handshake header: 4-byte "RTL0" magic, followed by a
|
||||
* 4-byte big endian tuner type and a 4-byte big endian tuner gain count (informational only).
|
||||
*/
|
||||
void readAndValidateHandshake(InputStream in) throws IOException {
|
||||
byte[] header = readFully(in, HANDSHAKE_LENGTH);
|
||||
|
||||
if (header[0] != 'R' || header[1] != 'T' || header[2] != 'L' || header[3] != '0') {
|
||||
throw new IOException("Unexpected rtl_tcp handshake magic from [" + mHost + ":" + mPort + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads exactly length bytes from the stream, blocking until enough bytes are available.
|
||||
*/
|
||||
static byte[] readFully(InputStream in, int length) throws IOException {
|
||||
byte[] buffer = new byte[length];
|
||||
int offset = 0;
|
||||
|
||||
while (offset < length) {
|
||||
int read = in.read(buffer, offset, length - offset);
|
||||
|
||||
if (read == -1) {
|
||||
throw new EOFException("Unexpected end of stream reading rtl_tcp handshake");
|
||||
}
|
||||
|
||||
offset += read;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes an rtl_tcp client command as a 5-byte packet: 1-byte command id followed by a 4-byte
|
||||
* big endian unsigned integer parameter.
|
||||
*/
|
||||
static byte[] encodeCommand(int commandId, int parameter) {
|
||||
return ByteBuffer.allocate(5).put((byte) commandId).putInt(parameter).array();
|
||||
}
|
||||
|
||||
private void sendCommand(OutputStream out, int commandId, int parameter) throws IOException {
|
||||
out.write(encodeCommand(commandId, parameter));
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the currently connected server, if any, ignoring (but logging and
|
||||
* reporting) any error. Used for applying live configuration changes from the tuner editor.
|
||||
*/
|
||||
private void trySendCommand(int commandId, int parameter) {
|
||||
OutputStream out = mOutputStream;
|
||||
|
||||
if (out != null) {
|
||||
try {
|
||||
sendCommand(out, commandId, parameter);
|
||||
} catch (IOException ioe) {
|
||||
mLog.error("Error sending command [" + commandId + "] to network tuner", ioe);
|
||||
setErrorMessage("Error sending command to network tuner - " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TunerType getTunerType() {
|
||||
return TunerType.RTL_TCP; // TODO: support more than this.
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return mHost;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return mPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount() {
|
||||
return READ_BUFFER_SIZE / 2; //2 bytes (I,Q) per complex sample
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTunedFrequency() throws SourceException {
|
||||
return mCenterFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTunedFrequency(long frequency) throws SourceException {
|
||||
mCenterFrequency = frequency;
|
||||
trySendCommand(CMD_SET_FREQUENCY, (int) frequency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getCurrentSampleRate() {
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
public boolean isGainAuto() {
|
||||
return mGainAuto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the gain mode and, when switching to manual, (re)sends the current gain value. Applies
|
||||
* immediately if connected; otherwise takes effect on the next connection.
|
||||
*/
|
||||
public void setGainAuto(boolean gainAuto) {
|
||||
mGainAuto = gainAuto;
|
||||
trySendCommand(CMD_SET_GAIN_MODE, gainAuto ? 0 : 1);
|
||||
if (!gainAuto) {
|
||||
trySendCommand(CMD_SET_GAIN, mGain);
|
||||
}
|
||||
}
|
||||
|
||||
public int getGain() {
|
||||
return mGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the manual gain value (tenths of dB). Only sent to the server when gain mode is manual.
|
||||
*/
|
||||
public void setGain(int gain) {
|
||||
mGain = gain;
|
||||
if (!mGainAuto) {
|
||||
trySendCommand(CMD_SET_GAIN, gain);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBiasTEnabled() {
|
||||
return mBiasTEnabled;
|
||||
}
|
||||
|
||||
public void setBiasTEnabled(boolean enabled) {
|
||||
mBiasTEnabled = enabled;
|
||||
trySendCommand(CMD_SET_BIAS_TEE, enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
return mAutoReconnect;
|
||||
}
|
||||
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
mAutoReconnect = autoReconnect;
|
||||
}
|
||||
|
||||
public long getSampleRateConfigured() {
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate. Applies immediately if connected; otherwise takes effect on the next
|
||||
* connection.
|
||||
*/
|
||||
public void setSampleRate(long sampleRate) {
|
||||
mSampleRate = sampleRate;
|
||||
mNativeBufferFactory.setSamplesPerMillisecond(sampleRate / 1000.0f);
|
||||
|
||||
try {
|
||||
mFrequencyController.setSampleRate((int) sampleRate);
|
||||
} catch (SourceException se) {
|
||||
mLog.error("Error updating sample rate", se);
|
||||
setErrorMessage("Error updating sample rate - " + se.getMessage());
|
||||
}
|
||||
|
||||
trySendCommand(CMD_SET_SAMPLE_RATE, (int) sampleRate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JSeparator;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
/**
|
||||
* Minimal tuner editor for a network (rtl_tcp) tuner that has not been started. Mirrors
|
||||
* RTL2832UnknownTunerEditor's pattern: only the id/status labels and the button panel are shown -
|
||||
* no frequency/sample-rate/gain controls, since there's no live tuner to configure yet.
|
||||
*/
|
||||
public class NetworkTunerDisabledEditor extends TunerEditor<NetworkTuner, NetworkTunerConfiguration> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NetworkTunerDisabledEditor(UserPreferences userPreferences, TunerManager tunerManager,
|
||||
DiscoveredTuner discoveredTuner) {
|
||||
super(userPreferences, tunerManager, discoveredTuner);
|
||||
init();
|
||||
tunerStatusUpdated();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][grow]"));
|
||||
|
||||
add(new JLabel("Tuner:"));
|
||||
add(getTunerIdLabel(), "wrap");
|
||||
|
||||
add(new JLabel("Status:"));
|
||||
add(getTunerStatusLabel(), "wrap");
|
||||
|
||||
add(getButtonPanel(), "span,align left");
|
||||
|
||||
add(new JSeparator(), "span,growx,push");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumTunableFrequency() {
|
||||
//Bogus value - matches NetworkTunerEditor's range since there's no live tuner to query yet.
|
||||
return 52000000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaximumTunableFrequency() {
|
||||
//Bogus value - matches NetworkTunerEditor's range since there's no live tuner to query yet.
|
||||
return 2200000000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void save() {
|
||||
//No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tunerStatusUpdated() {
|
||||
setLoading(true);
|
||||
getTunerIdLabel().setText(getDiscoveredTuner().getId());
|
||||
|
||||
String status = getDiscoveredTuner().getTunerStatus().toString();
|
||||
if (getDiscoveredTuner().hasErrorMessage()) {
|
||||
status += " - " + getDiscoveredTuner().getErrorMessage();
|
||||
}
|
||||
getTunerStatusLabel().setText(status);
|
||||
getButtonPanel().updateControls();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTunerLockState(boolean locked) {
|
||||
getFrequencyPanel().updateControls();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.JToggleButton;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
public class NetworkTunerEditor extends TunerEditor<NetworkTuner, NetworkTunerConfiguration> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger mLog = LoggerFactory.getLogger(NetworkTunerEditor.class);
|
||||
|
||||
//R820T total gain table (tenths of dB), from librtlsdr's rtlsdr_get_tuner_gains() - most rtl_tcp
|
||||
//servers are backed by an R820T/R820T2 dongle and report this same gain table.
|
||||
private static final Integer[] R820T_GAINS_TENTHS_DB = {0, 9, 14, 27, 37, 77, 87, 125, 144, 157, 166, 197, 207,
|
||||
229, 254, 280, 297, 328, 338, 364, 372, 386, 402, 421, 434, 439, 445, 480, 496};
|
||||
|
||||
private JButton mTunerInfoButton;
|
||||
private JToggleButton mBiasTButton;
|
||||
private JComboBox<SampleRate> mSampleRateCombo;
|
||||
private JCheckBox mGainAutoCheckBox;
|
||||
private JComboBox<Integer> mGainCombo;
|
||||
private JCheckBox mAutoReconnectCheckBox;
|
||||
|
||||
public NetworkTunerEditor(UserPreferences userPreferences, TunerManager tunerManager,
|
||||
DiscoveredTuner discoveredTuner) {
|
||||
super(userPreferences, tunerManager, discoveredTuner);
|
||||
init();
|
||||
tunerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTunerLockState(boolean locked) {
|
||||
getFrequencyPanel().updateControls();
|
||||
//Matches the USB RTL2832/R8x tuner pattern: sample rate can't change while channels are active. Gain and
|
||||
//Bias-T don't affect channelizing, so they remain adjustable regardless of lock state.
|
||||
getSampleRateCombo().setEnabled(hasTuner() && !locked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumTunableFrequency() {
|
||||
return 52000000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaximumTunableFrequency() {
|
||||
return 2200000000l;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tunerStatusUpdated() {
|
||||
setLoading(true);
|
||||
|
||||
if (hasTuner()) {
|
||||
getTunerIdLabel().setText(getTuner().getPreferredName());
|
||||
} else {
|
||||
getTunerIdLabel().setText(getDiscoveredTuner().getId());
|
||||
}
|
||||
|
||||
String status = getDiscoveredTuner().getTunerStatus().toString();
|
||||
if (getDiscoveredTuner().hasErrorMessage()) {
|
||||
status += " - " + getDiscoveredTuner().getErrorMessage();
|
||||
}
|
||||
getTunerStatusLabel().setText(status);
|
||||
getButtonPanel().updateControls();
|
||||
getFrequencyPanel().updateControls();
|
||||
|
||||
getTunerInfoButton().setEnabled(hasTuner());
|
||||
getBiasTButton().setEnabled(hasTuner());
|
||||
getSampleRateCombo().setEnabled(hasTuner());
|
||||
getGainAutoCheckBox().setEnabled(hasTuner());
|
||||
getAutoReconnectCheckBox().setEnabled(hasTuner());
|
||||
|
||||
if (hasConfiguration()) {
|
||||
getBiasTButton().setSelected(getConfiguration().isBiasTEnabled());
|
||||
getSampleRateCombo().setSelectedItem(closestSampleRate(getConfiguration().getSampleRate()));
|
||||
getGainAutoCheckBox().setSelected(getConfiguration().isGainAuto());
|
||||
getGainCombo().setSelectedItem(closestGain(getConfiguration().getGain()));
|
||||
getGainCombo().setEnabled(hasTuner() && !getConfiguration().isGainAuto());
|
||||
getAutoReconnectCheckBox().setSelected(getConfiguration().isAutoReconnect());
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][grow]"));
|
||||
|
||||
add(new JLabel("Tuner:"));
|
||||
add(getTunerIdLabel());
|
||||
add(getTunerInfoButton());
|
||||
|
||||
add(new JLabel("Status:"));
|
||||
add(getTunerStatusLabel());
|
||||
add(getBiasTButton(), "wrap");
|
||||
|
||||
add(getButtonPanel(), "span,align left");
|
||||
add(new JSeparator(), "span,growx,push");
|
||||
|
||||
add(new JLabel("Frequency (MHz):"));
|
||||
add(getFrequencyPanel(), "wrap");
|
||||
|
||||
add(new JLabel("Sample Rate:"));
|
||||
add(getSampleRateCombo(), "wrap");
|
||||
|
||||
add(new JLabel("Gain:"));
|
||||
add(getGainAutoCheckBox(), "split 2");
|
||||
add(getGainCombo(), "wrap");
|
||||
|
||||
add(new JLabel("Auto-Reconnect:"));
|
||||
add(getAutoReconnectCheckBox(), "wrap");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bias-T toggle button, matching the USB RTL2832/R8x tuner editor's top-row placement.
|
||||
*/
|
||||
private JToggleButton getBiasTButton() {
|
||||
if (mBiasTButton == null) {
|
||||
mBiasTButton = new JToggleButton("Bias-T");
|
||||
mBiasTButton.setEnabled(false);
|
||||
mBiasTButton.addActionListener(e -> {
|
||||
if (!isLoading()) {
|
||||
boolean enabled = mBiasTButton.isSelected();
|
||||
|
||||
if (hasTuner()) {
|
||||
getTuner().getTunerController().setBiasTEnabled(enabled);
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mBiasTButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hyperlink-style button that shows the network tuner's connection details.
|
||||
*/
|
||||
private JButton getTunerInfoButton() {
|
||||
if (mTunerInfoButton == null) {
|
||||
mTunerInfoButton = new JButton("Info");
|
||||
mTunerInfoButton.setEnabled(false);
|
||||
mTunerInfoButton.addActionListener(e -> JOptionPane.showMessageDialog(NetworkTunerEditor.this,
|
||||
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
|
||||
}
|
||||
|
||||
return mTunerInfoButton;
|
||||
}
|
||||
|
||||
private String getTunerInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<html><h3>rtl_tcp Network Tuner</h3>");
|
||||
sb.append("<b>Host: </b>").append(getConfiguration().getHost()).append("<br>");
|
||||
sb.append("<b>Port: </b>").append(getConfiguration().getPort()).append("<br>");
|
||||
sb.append("<b>Tuner Type: </b>rtl_tcp<br>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private JComboBox<SampleRate> getSampleRateCombo() {
|
||||
if (mSampleRateCombo == null) {
|
||||
mSampleRateCombo = new JComboBox<>(SampleRate.values());
|
||||
mSampleRateCombo.setEnabled(false);
|
||||
mSampleRateCombo.addActionListener(e -> {
|
||||
if (!isLoading()) {
|
||||
SampleRate sampleRate = (SampleRate) mSampleRateCombo.getSelectedItem();
|
||||
|
||||
if (sampleRate != null) {
|
||||
if (hasTuner()) {
|
||||
getTuner().getTunerController().setSampleRate(sampleRate.getRate());
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mSampleRateCombo;
|
||||
}
|
||||
|
||||
private JCheckBox getGainAutoCheckBox() {
|
||||
if (mGainAutoCheckBox == null) {
|
||||
mGainAutoCheckBox = new JCheckBox("Auto");
|
||||
mGainAutoCheckBox.addActionListener(e -> {
|
||||
boolean gainAuto = mGainAutoCheckBox.isSelected();
|
||||
getGainCombo().setEnabled(hasTuner() && !gainAuto);
|
||||
|
||||
if (!isLoading()) {
|
||||
if (hasTuner()) {
|
||||
getTuner().getTunerController().setGainAuto(gainAuto);
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mGainAutoCheckBox;
|
||||
}
|
||||
|
||||
private JComboBox<Integer> getGainCombo() {
|
||||
if (mGainCombo == null) {
|
||||
mGainCombo = new JComboBox<>(R820T_GAINS_TENTHS_DB);
|
||||
mGainCombo.setEnabled(false);
|
||||
mGainCombo.setToolTipText("Gain in tenths of dB. Uncheck Auto to enable manual gain.");
|
||||
mGainCombo.addActionListener(e -> {
|
||||
if (!isLoading()) {
|
||||
Integer gain = (Integer) mGainCombo.getSelectedItem();
|
||||
|
||||
if (gain != null) {
|
||||
if (hasTuner()) {
|
||||
getTuner().getTunerController().setGain(gain);
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mGainCombo;
|
||||
}
|
||||
|
||||
private JCheckBox getAutoReconnectCheckBox() {
|
||||
if (mAutoReconnectCheckBox == null) {
|
||||
mAutoReconnectCheckBox = new JCheckBox();
|
||||
mAutoReconnectCheckBox.setToolTipText("Automatically reconnect if the connection to the rtl_tcp server is lost");
|
||||
mAutoReconnectCheckBox.addActionListener(e -> {
|
||||
if (!isLoading()) {
|
||||
boolean autoReconnect = mAutoReconnectCheckBox.isSelected();
|
||||
|
||||
if (hasTuner()) {
|
||||
getTuner().getTunerController().setAutoReconnect(autoReconnect);
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mAutoReconnectCheckBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the SampleRate enum value closest to the specified sample rate (Hz).
|
||||
*/
|
||||
private static SampleRate closestSampleRate(long sampleRate) {
|
||||
SampleRate closest = SampleRate.values()[0];
|
||||
|
||||
for (SampleRate candidate : SampleRate.values()) {
|
||||
if (Math.abs(candidate.getRate() - sampleRate) < Math.abs(closest.getRate() - sampleRate)) {
|
||||
closest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the R820T gain table entry closest to the specified gain value (tenths of dB).
|
||||
*/
|
||||
private static int closestGain(int gain) {
|
||||
int closest = R820T_GAINS_TENTHS_DB[0];
|
||||
|
||||
for (int candidate : R820T_GAINS_TENTHS_DB) {
|
||||
if (Math.abs(candidate - gain) < Math.abs(closest - gain)) {
|
||||
closest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
if (hasConfiguration() && !isLoading()) {
|
||||
NetworkTunerConfiguration config = getConfiguration();
|
||||
config.setFrequency(getFrequencyControl().getFrequency());
|
||||
getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency());
|
||||
getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency());
|
||||
|
||||
SampleRate sampleRate = (SampleRate) getSampleRateCombo().getSelectedItem();
|
||||
if (sampleRate != null) {
|
||||
config.setSampleRate(sampleRate.getRate());
|
||||
}
|
||||
|
||||
config.setGainAuto(getGainAutoCheckBox().isSelected());
|
||||
|
||||
Integer gain = (Integer) getGainCombo().getSelectedItem();
|
||||
if (gain != null) {
|
||||
config.setGain(gain);
|
||||
}
|
||||
|
||||
config.setBiasTEnabled(getBiasTButton().isSelected());
|
||||
config.setAutoReconnect(getAutoReconnectCheckBox().isSelected());
|
||||
saveConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,12 @@ import com.jidesoft.swing.JideSplitPane;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationManager;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredNetworkTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredRecordingTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerStatus;
|
||||
import io.github.dsheirer.source.tuner.network.AddNetworkTunerDialog;
|
||||
import io.github.dsheirer.source.tuner.recording.AddRecordingTunerDialog;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
@@ -70,7 +72,8 @@ public class TunerViewPanel extends JPanel
|
||||
private TableRowSorter<DiscoveredTunerModel> mRowSorter;
|
||||
private JideSplitPane mSplitPane;
|
||||
private JButton mAddRecordingButton;
|
||||
private JButton mRemoveRecordingButton;
|
||||
private JButton mRemoveTunerButton;
|
||||
private JButton mAddNetworkButton;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -100,7 +103,7 @@ public class TunerViewPanel extends JPanel
|
||||
mTunerTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
mTunerTable.getSelectionModel().addListSelectionListener(event ->
|
||||
{
|
||||
getRemoveRecordingButton().setEnabled(false);
|
||||
getRemoveTunerButton().setEnabled(false);
|
||||
|
||||
if(!event.getValueIsAdjusting())
|
||||
{
|
||||
@@ -112,7 +115,7 @@ public class TunerViewPanel extends JPanel
|
||||
|
||||
DiscoveredTuner selected = mDiscoveredTunerModel.getDiscoveredTuner(modelRow);
|
||||
mDiscoveredTunerEditor.setItem(selected);
|
||||
getRemoveRecordingButton().setEnabled(selected instanceof DiscoveredRecordingTuner);
|
||||
getRemoveTunerButton().setEnabled(selected instanceof DiscoveredRecordingTuner || selected instanceof DiscoveredNetworkTuner);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -192,7 +195,8 @@ public class TunerViewPanel extends JPanel
|
||||
JPanel buttonPanel = new JPanel();
|
||||
buttonPanel.setLayout(new MigLayout("insets 0 1 3 0", "", ""));
|
||||
buttonPanel.add(getAddRecordingButton());
|
||||
buttonPanel.add(getRemoveRecordingButton());
|
||||
buttonPanel.add(getAddNetworkButton());
|
||||
buttonPanel.add(getRemoveTunerButton());
|
||||
tunerTablePanel.add(buttonPanel);
|
||||
|
||||
tunerTablePanel.setPreferredSize(new Dimension(200,200));
|
||||
@@ -226,13 +230,13 @@ public class TunerViewPanel extends JPanel
|
||||
return mAddRecordingButton;
|
||||
}
|
||||
|
||||
private JButton getRemoveRecordingButton()
|
||||
private JButton getRemoveTunerButton()
|
||||
{
|
||||
if(mRemoveRecordingButton == null)
|
||||
if(mRemoveTunerButton == null)
|
||||
{
|
||||
mRemoveRecordingButton = new JButton("Remove Recording Tuner");
|
||||
mRemoveRecordingButton.setEnabled(false);
|
||||
mRemoveRecordingButton.addActionListener(e -> {
|
||||
mRemoveTunerButton = new JButton("Remove Tuner");
|
||||
mRemoveTunerButton.setEnabled(false);
|
||||
mRemoveTunerButton.addActionListener(e -> {
|
||||
int[] indexes = mTunerTable.getSelectionModel().getSelectedIndices();
|
||||
|
||||
//With single selection mode this should always be length one
|
||||
@@ -241,18 +245,34 @@ public class TunerViewPanel extends JPanel
|
||||
int modelIndex = mTunerTable.convertRowIndexToModel(indexes[0]);
|
||||
DiscoveredTuner selected = mDiscoveredTunerModel.getDiscoveredTuner(modelIndex);
|
||||
|
||||
if(selected instanceof DiscoveredRecordingTuner discoveredRecordingTuner)
|
||||
if(selected instanceof DiscoveredRecordingTuner || selected instanceof DiscoveredNetworkTuner)
|
||||
{
|
||||
mLog.info("Removing Tuner: " + discoveredRecordingTuner);
|
||||
discoveredRecordingTuner.stop();
|
||||
mTunerConfigurationManager.removeTunerConfiguration(discoveredRecordingTuner.getTunerConfiguration());
|
||||
EventQueue.invokeLater(() -> mDiscoveredTunerModel.removeDiscoveredTuner(discoveredRecordingTuner));
|
||||
mLog.info("Removing Tuner: " + selected);
|
||||
selected.stop();
|
||||
mTunerConfigurationManager.removeTunerConfiguration(selected.getTunerConfiguration());
|
||||
EventQueue.invokeLater(() -> mDiscoveredTunerModel.removeDiscoveredTuner(selected));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mRemoveRecordingButton;
|
||||
return mRemoveTunerButton;
|
||||
}
|
||||
|
||||
private JButton getAddNetworkButton()
|
||||
{
|
||||
if (mAddNetworkButton == null)
|
||||
{
|
||||
mAddNetworkButton = new JButton("Add Network Tuner");
|
||||
mAddNetworkButton.addActionListener(e ->
|
||||
{
|
||||
AddNetworkTunerDialog dialog = new AddNetworkTunerDialog(mUserPreferences, mDiscoveredTunerModel, mTunerConfigurationManager);
|
||||
dialog.setLocationRelativeTo(TunerViewPanel.this);
|
||||
EventQueue.invokeLater(() -> dialog.setVisible(true));
|
||||
});
|
||||
}
|
||||
|
||||
return mAddNetworkButton;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2026 Jared Szechy
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Unit tests for the rtl_tcp wire protocol helpers in {@link NetworkTunerController}: command
|
||||
* encoding and handshake header validation.
|
||||
*/
|
||||
public class NetworkTunerControllerTest {
|
||||
@Test
|
||||
public void testEncodeCommandProducesFiveByteBigEndianPacket() {
|
||||
//Command 0x01 (set frequency) with parameter 100_000_000 (100 MHz)
|
||||
byte[] expected = ByteBuffer.allocate(5).put((byte) 0x01).putInt(100_000_000).array();
|
||||
byte[] actual = NetworkTunerController.encodeCommand(0x01, 100_000_000);
|
||||
assertEquals(5, actual.length);
|
||||
assertArrayEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeCommandNegativeParameterEncodesAsUnsignedBigEndian() {
|
||||
//rtl_tcp treats the 4-byte parameter as an unsigned 32-bit value - verify negative Java
|
||||
//ints (used to represent values > Integer.MAX_VALUE) round-trip through the same bytes
|
||||
//as ByteBuffer.putInt() would produce.
|
||||
byte[] expected = ByteBuffer.allocate(5).put((byte) 0x02).putInt(-1).array();
|
||||
byte[] actual = NetworkTunerController.encodeCommand(0x02, -1);
|
||||
assertArrayEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAndValidateHandshakeAcceptsValidMagic() throws IOException {
|
||||
byte[] header = new byte[]{'R', 'T', 'L', '0', 0, 0, 0, 0, 0, 0, 0, 1};
|
||||
NetworkTunerController controller = createController();
|
||||
//Should not throw
|
||||
controller.readAndValidateHandshake(new ByteArrayInputStream(header));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAndValidateHandshakeRejectsInvalidMagic() {
|
||||
byte[] header = new byte[]{'X', 'X', 'X', 'X', 0, 0, 0, 0, 0, 0, 0, 1};
|
||||
NetworkTunerController controller = createController();
|
||||
InputStream in = new ByteArrayInputStream(header);
|
||||
assertThrows(IOException.class, () -> controller.readAndValidateHandshake(in));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAndValidateHandshakeRejectsTruncatedHeader() {
|
||||
byte[] header = new byte[]{'R', 'T', 'L', '0'};
|
||||
NetworkTunerController controller = createController();
|
||||
InputStream in = new ByteArrayInputStream(header);
|
||||
assertThrows(IOException.class, () -> controller.readAndValidateHandshake(in));
|
||||
}
|
||||
|
||||
private static NetworkTunerController createController() {
|
||||
NetworkTunerConfiguration config = NetworkTunerConfiguration.create();
|
||||
config.setHost("localhost");
|
||||
config.setPort((short) 1234);
|
||||
return new NetworkTunerController(new ITunerErrorListener() {
|
||||
@Override
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tunerRemoved() {
|
||||
}
|
||||
}, config);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user