#2600 LimeSDR support (#2601)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-08 06:03:13 -05:00
committed by GitHub
parent aff34fa5eb
commit 657cbf5c55
14 changed files with 2006 additions and 2 deletions

View File

@@ -129,6 +129,7 @@ dependencies {
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhackrf:2026.01.3' //HackRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhydrasdr:1.1.4' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jlimesdr:25.1.0.1' //LimeSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jsdrplay:3.15.3' //SDRplay RSP tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'

View File

@@ -49,6 +49,14 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
}
//jLimeSDR is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jLimeSDR/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jlimesdr' }
}
//jsdrplay is published as a GitHub release. The jar does not contain the SDRplay API native library, which
//users install separately from sdrplay.com
ivy {

View File

@@ -32,6 +32,7 @@ public enum TunerClass
FUNCUBE_DONGLE_PRO_PLUS("Funcube Dongle Pro+" ),
HACKRF("HackRF" ),
HYDRASDR("HydraSDR"),
LIMESDR("LimeSDR"),
RTL2832("RTL-2832"),
RSP("RSP"),
TEST_TUNER("Test"),
@@ -56,7 +57,7 @@ public enum TunerClass
}
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, BLADERF, HACKRF,
HYDRASDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
HYDRASDR, LIMESDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerClass> FUNCUBE_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
@@ -146,6 +147,9 @@ public enum TunerClass
return BLADERF;
case 0x38AF0001: //HYDRASDR RFONE (official VID 38af, PID 0001)
return HYDRASDR;
case 0x1D506108: //LIMESDR-USB
case 0x0403601F: //LIMESDR MINI (FTDI FT601)
return LIMESDR;
case 0x1D5060A1:
return AIRSPY;
case 0x03EB800C:

View File

@@ -54,6 +54,10 @@ import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTuner;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerConfiguration;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerController;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerEditor;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTuner;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerConfiguration;
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.recording.RecordingTunerConfiguration;
@@ -313,6 +317,8 @@ public class TunerFactory
return new HackRFTuner(new HackRFTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case HYDRASDR:
return new HydraSdrTuner(new HydraSdrTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case LIMESDR:
return new LimeSdrTuner(new LimeSdrTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case RTL2832:
return new RTL2832Tuner(new RTL2832TunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
default:
@@ -352,6 +358,8 @@ public class TunerFactory
return new HackRFTunerConfiguration(uniqueID);
case HYDRASDR_R828D:
return new HydraSdrTunerConfiguration(uniqueID);
case LIMESDR:
return new LimeSdrTunerConfiguration(uniqueID);
case RAFAELMICRO_R820T:
return new R820TTunerConfiguration(uniqueID);
case RAFAELMICRO_R828D:
@@ -399,6 +407,8 @@ public class TunerFactory
return new HackRFTunerEditor(userPreferences, tunerManager, discoveredTuner);
case HYDRASDR:
return new HydraSdrTunerEditor(userPreferences, tunerManager, discoveredTuner);
case LIMESDR:
return new LimeSdrTunerEditor(userPreferences, tunerManager, discoveredTuner);
case RSP:
if(discoveredTuner instanceof DiscoveredRspTuner discoveredRspTuner)
{

View File

@@ -39,6 +39,7 @@ public enum TunerType
HACKRF_JAWBREAKER("Jawbreaker"),
HACKRF_RAD1O("RAD1O"),
HYDRASDR_R828D("R828D"),
LIMESDR("LimeSDR"),
RAFAELMICRO_R820T("R820T"),
RAFAELMICRO_R828D("R828D"),
RSP_1("RSP1"),
@@ -76,7 +77,7 @@ public enum TunerType
* Supported USB tuner types
*/
public static final EnumSet<TunerType> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY_R820T, BLADERF, ELONICS_E4000, FITIPOWER_FC0012,
HACKRF_JAWBREAKER, HACKRF_RAD1O, HACKRF_ONE, HACKRF_PRO, HYDRASDR_R828D, RAFAELMICRO_R820T);
HACKRF_JAWBREAKER, HACKRF_RAD1O, HACKRF_ONE, HACKRF_PRO, HYDRASDR_R828D, LIMESDR, RAFAELMICRO_R820T);
/**
* Supported sound card tuner types

View File

@@ -31,6 +31,7 @@ import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerConfiguration;
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.recording.RecordingTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.fc0012.FC0012TunerConfiguration;
@@ -56,6 +57,7 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
@JsonSubTypes.Type(value = FCD2TunerConfiguration.class, name = "fcd2TunerConfiguration"),
@JsonSubTypes.Type(value = HackRFTunerConfiguration.class, name = "hackRFTunerConfiguration"),
@JsonSubTypes.Type(value = HydraSdrTunerConfiguration.class, name = "hydraSdrTunerConfiguration"),
@JsonSubTypes.Type(value = LimeSdrTunerConfiguration.class, name = "limeSdrTunerConfiguration"),
@JsonSubTypes.Type(value = RecordingTunerConfiguration.class, name = "recordingTunerConfiguration"),
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),

View File

@@ -0,0 +1,35 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
/**
* LimeSDR device details for display.
* @param serial board serial number, 16 hex digits
* @param board name, such as LimeSDR-USB or LimeSDR-Mini
* @param firmware version
* @param hardware version
* @param gateware (FPGA) version
* @param connection USB link, such as USB3.0
* @param library version of LimeSuiteNG
*/
public record LimeSdrDeviceInformation(String serial, String board, String firmware, String hardware, String gateware,
String connection, String library)
{
}

View File

@@ -0,0 +1,112 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
import java.util.ArrayList;
import java.util.List;
/**
* LimeSDR sample rates offered to the user. The LMS7002M supports any rate in its range, so these are rates that divide
* evenly into polyphase channelizer channels.
*/
public enum LimeSdrSampleRate
{
RATE_1_25(1_250_000, "1.25 MHz"),
RATE_2_5(2_500_000, "2.50 MHz"),
RATE_5_0(5_000_000, "5.00 MHz"),
RATE_7_5(7_500_000, "7.50 MHz"),
RATE_10_0(10_000_000, "10.00 MHz"),
RATE_12_5(12_500_000, "12.50 MHz"),
RATE_15_0(15_000_000, "15.00 MHz"),
RATE_20_0(20_000_000, "20.00 MHz"),
RATE_25_0(25_000_000, "25.00 MHz"),
RATE_30_0(30_000_000, "30.00 MHz"),
RATE_30_72(30_720_000, "30.72 MHz"),
RATE_40_0(40_000_000, "40.00 MHz"),
RATE_50_0(50_000_000, "50.00 MHz"),
RATE_60_0(60_000_000, "60.00 MHz"),
RATE_61_44(61_440_000, "61.44 MHz");
public static final LimeSdrSampleRate DEFAULT = RATE_10_0;
private final int mRate;
private final String mLabel;
LimeSdrSampleRate(int rate, String label)
{
mRate = rate;
mLabel = label;
}
/**
* Sample rate in Hertz
*/
public int getRate()
{
return mRate;
}
@Override
public String toString()
{
return mLabel;
}
/**
* Sample rates within the device range, which is the lower of what the board and its USB connection support.
* @param minimumRate in Hertz
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/
public static List<LimeSdrSampleRate> getRates(double minimumRate, double maximumRate)
{
List<LimeSdrSampleRate> rates = new ArrayList<>();
for(LimeSdrSampleRate rate : values())
{
if(rate.getRate() >= minimumRate && rate.getRate() <= maximumRate)
{
rates.add(rate);
}
}
return rates;
}
/**
* Finds the highest rate from the list that doesn't exceed the requested rate.
* @param rates to search, lowest to highest
* @param requested rate in Hertz
* @return closest rate, or the lowest rate when the requested rate is below all of them.
*/
public static LimeSdrSampleRate getClosest(List<LimeSdrSampleRate> rates, int requested)
{
LimeSdrSampleRate closest = rates.getFirst();
for(LimeSdrSampleRate rate : rates)
{
if(rate.getRate() <= requested)
{
closest = rate;
}
}
return closest;
}
}

View File

@@ -0,0 +1,86 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
import io.github.dsheirer.preference.source.ChannelizerType;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerClass;
/**
* LimeSDR tuner
*/
public class LimeSdrTuner extends Tuner
{
//The USB link carries 12-bit I and Q samples, packed into 3 bytes
private static final int LINK_BITS_PER_SAMPLE = 24;
private static final double SAMPLE_SIZE_12_BIT = 12.0;
/**
* Constructs an instance
* @param controller for the LimeSDR
* @param tunerErrorListener to listen for errors from this tuner
* @param channelizerType for the channelizer
*/
public LimeSdrTuner(LimeSdrTunerController controller, ITunerErrorListener tunerErrorListener,
ChannelizerType channelizerType)
{
super(controller, tunerErrorListener, channelizerType);
}
/**
* LimeSDR tuner controller
*/
public LimeSdrTunerController getController()
{
return (LimeSdrTunerController)getTunerController();
}
@Override
public String getPreferredName()
{
LimeSdrDeviceInformation info = getController().getDeviceInfo();
return (info != null ? info.board() : "LimeSDR") + " " + getUniqueID();
}
@Override
public String getUniqueID()
{
return getController().getDeviceInfo().serial();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.LIMESDR;
}
@Override
public double getSampleSize()
{
return SAMPLE_SIZE_12_BIT;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return LimeSdrSampleRate.RATE_61_44.getRate() * LINK_BITS_PER_SAMPLE;
}
}

View File

@@ -0,0 +1,97 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
/**
* LimeSDR tuner configuration
*/
public class LimeSdrTunerConfiguration extends TunerConfiguration
{
private int mSampleRate = LimeSdrSampleRate.DEFAULT.getRate();
private int mGain = LimeSdrTunerController.GAIN_DEFAULT;
private String mAntenna = LimeSdrTunerController.ANTENNA_DEFAULT;
/**
* Default constructor for Jackson
*/
public LimeSdrTunerConfiguration()
{
super(LimeSdrTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, LimeSdrTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ);
}
/**
* Constructs an instance
* @param uniqueID for the tuner
*/
public LimeSdrTunerConfiguration(String uniqueID)
{
super(uniqueID);
}
@Override
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public TunerType getTunerType()
{
return TunerType.LIMESDR;
}
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
/**
* Combined receive gain in dB, 0 to 73.
*/
@JacksonXmlProperty(isAttribute = true, localName = "gain")
public int getGain()
{
return mGain;
}
public void setGain(int gain)
{
mGain = gain;
}
/**
* Receive antenna port name, such as LNAW.
*/
@JacksonXmlProperty(isAttribute = true, localName = "antenna")
public String getAntenna()
{
return mAntenna;
}
public void setAntenna(String antenna)
{
mAntenna = antenna;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
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.manager.TunerStatus;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import java.io.Serial;
import java.util.List;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.SpinnerNumberModel;
/**
* LimeSDR tuner editor
*/
public class LimeSdrTunerEditor extends TunerEditor<LimeSdrTuner, LimeSdrTunerConfiguration>
{
@Serial
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(LimeSdrTunerEditor.class);
private JButton mTunerInfoButton;
private JComboBox<LimeSdrSampleRate> mSampleRateCombo;
private JComboBox<String> mAntennaCombo;
private JSlider mGainSlider;
private JLabel mGainValueLabel;
/**
* Constructs an instance
* @param userPreferences for wide-band recordings
* @param tunerManager for saving tuner configuration
* @param discoveredTuner for the optionally usable LimeSDR tuner and controller
*/
public LimeSdrTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
{
super(userPreferences, tunerManager, discoveredTuner);
init();
tunerStatusUpdated();
}
@Override
public long getMinimumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMinimumTunableFrequency() :
LimeSdrTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ;
}
@Override
public long getMaximumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMaximumTunableFrequency() :
LimeSdrTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ;
}
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(), "wrap");
add(getButtonPanel(), "span,align left");
add(new JSeparator(), "span,growx");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("Antenna:"));
add(getAntennaCombo(), "wrap");
add(new JSeparator(), "span,growx");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
//Empty row that takes all extra vertical space so that the controls stay stacked at the top of the editor.
add(new JLabel(), "span,pushy");
}
@Override
protected void tunerStatusUpdated()
{
setLoading(true);
boolean started = hasStartedTuner();
getTunerIdLabel().setText(started ? getTuner().getPreferredName() : getDiscoveredTuner().getId());
String status = getDiscoveredTuner().getTunerStatus().toString();
if(getDiscoveredTuner().hasErrorMessage())
{
status += " - " + getDiscoveredTuner().getErrorMessage();
}
getTunerStatusLabel().setText(status);
getButtonPanel().updateControls();
getFrequencyPanel().updateControls();
getTunerInfoButton().setEnabled(started);
if(started)
{
LimeSdrTunerController controller = getTuner().getController();
List<LimeSdrSampleRate> rates = controller.getSampleRates();
getSampleRateCombo().setModel(new DefaultComboBoxModel<>(rates.toArray(new LimeSdrSampleRate[0])));
getSampleRateCombo().setSelectedItem(controller.getLimeSdrSampleRate());
getSampleRateCombo().setEnabled(!controller.isLockedSampleRate());
List<String> antennas = controller.getAntennas();
getAntennaCombo().setModel(new DefaultComboBoxModel<>(antennas.toArray(new String[0])));
getAntennaCombo().setSelectedItem(controller.getAntenna());
getAntennaCombo().setEnabled(true);
getGainSlider().setValue(controller.getGain());
getGainSlider().setEnabled(true);
getGainValueLabel().setEnabled(true);
}
else
{
getSampleRateCombo().setModel(new DefaultComboBoxModel<>());
getSampleRateCombo().setEnabled(false);
getAntennaCombo().setModel(new DefaultComboBoxModel<>());
getAntennaCombo().setEnabled(false);
getGainSlider().setEnabled(false);
getGainValueLabel().setEnabled(false);
}
updateGainLabel();
setLoading(false);
}
/**
* Indicates if the tuner has finished starting, since a status update can arrive while it's starting or restarting.
*/
private boolean hasStartedTuner()
{
return hasTuner() && getTuner().getController().getAntenna() != null;
}
private void updateGainLabel()
{
getGainValueLabel().setText(getGainSlider().getValue() + " dB");
}
/**
* Button that provides tuner information
*/
private JButton getTunerInfoButton()
{
if(mTunerInfoButton == null)
{
mTunerInfoButton = new JButton("Info");
mTunerInfoButton.setEnabled(false);
mTunerInfoButton.addActionListener(_ -> JOptionPane.showMessageDialog(LimeSdrTunerEditor.this,
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
}
return mTunerInfoButton;
}
private JComboBox<LimeSdrSampleRate> getSampleRateCombo()
{
if(mSampleRateCombo == null)
{
mSampleRateCombo = new JComboBox<>();
mSampleRateCombo.setEnabled(false);
mSampleRateCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
LimeSdrSampleRate rate = (LimeSdrSampleRate)mSampleRateCombo.getSelectedItem();
try
{
getTuner().getController().setSampleRate(rate);
//Adjust the min/max values for the sample rate.
adjustForSampleRate(rate.getRate());
save();
}
catch(Exception e1)
{
mLog.error("Error setting LimeSDR sample rate", e1);
JOptionPane.showMessageDialog(LimeSdrTunerEditor.this, "Couldn't set sample rate to " + rate);
}
}
});
}
return mSampleRateCombo;
}
private JComboBox<String> getAntennaCombo()
{
if(mAntennaCombo == null)
{
mAntennaCombo = new JComboBox<>();
mAntennaCombo.setEnabled(false);
mAntennaCombo.setToolTipText("Receive port: LNAW wideband (default), LNAL low band, LNAH high band");
mAntennaCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
String antenna = (String)mAntennaCombo.getSelectedItem();
try
{
getTuner().getController().setAntenna(antenna);
save();
}
catch(Exception e1)
{
mLog.error("Error setting LimeSDR antenna", e1);
JOptionPane.showMessageDialog(mAntennaCombo, "Couldn't set antenna to " + antenna);
}
}
});
}
return mAntennaCombo;
}
private JSlider getGainSlider()
{
if(mGainSlider == null)
{
mGainSlider = new JSlider(JSlider.HORIZONTAL, LimeSdrTunerController.GAIN_MINIMUM,
LimeSdrTunerController.GAIN_MAXIMUM, LimeSdrTunerController.GAIN_DEFAULT);
mGainSlider.setEnabled(false);
mGainSlider.setMajorTickSpacing(5);
mGainSlider.setPaintTicks(true);
mGainSlider.addChangeListener(_ ->
{
int gain = mGainSlider.getValue();
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setGain(gain);
save();
}
catch(Exception e)
{
mLog.error("Couldn't set LimeSDR gain to:" + gain, e);
JOptionPane.showMessageDialog(mGainSlider, "Couldn't set gain value to " + gain);
}
}
updateGainLabel();
});
}
return mGainSlider;
}
private JLabel getGainValueLabel()
{
if(mGainValueLabel == null)
{
mGainValueLabel = new JLabel("0 dB");
mGainValueLabel.setEnabled(false);
}
return mGainValueLabel;
}
@Override
public void save()
{
if(hasConfiguration() && !isLoading())
{
getConfiguration().setFrequency(getFrequencyControl().getFrequency());
getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency());
getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency());
double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
getConfiguration().setFrequencyCorrection(value);
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
if(getSampleRateCombo().getSelectedItem() instanceof LimeSdrSampleRate rate)
{
getConfiguration().setSampleRate(rate.getRate());
}
if(getAntennaCombo().getSelectedItem() instanceof String antenna)
{
getConfiguration().setAntenna(antenna);
}
getConfiguration().setGain(getGainSlider().getValue());
saveConfiguration();
}
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
getSampleRateCombo().setToolTipText(locked ? "Sample Rate is locked. Disable decoding channels to unlock." :
"Select a sample rate for the tuner");
}
private String getTunerInfo()
{
if(getDiscoveredTuner().getTunerStatus() == TunerStatus.ERROR)
{
return getDiscoveredTuner().getErrorMessage();
}
if(hasTuner())
{
LimeSdrDeviceInformation info = getTuner().getController().getDeviceInfo();
StringBuilder sb = new StringBuilder();
sb.append("<html><h3>LimeSDR Tuner</h3>");
sb.append("<b>Board: </b>").append(info.board()).append("<br>");
sb.append("<b>Serial: </b>").append(info.serial()).append("<br>");
sb.append("<b>Hardware: </b>").append(info.hardware()).append("<br>");
sb.append("<b>Firmware: </b>").append(info.firmware()).append("<br>");
sb.append("<b>Gateware: </b>").append(info.gateware()).append("<br>");
sb.append("<b>Connection: </b>").append(info.connection()).append("<br>");
sb.append("<b>LimeSuiteNG: </b>").append(info.library()).append("<br>");
return sb.toString();
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
# LimeSDR udev rules
# Grants access to the logged-in user (uaccess) and to the plugdev group.
# LimeSDR-USB
SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6108", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# LimeSDR Mini and LimeSDR Mini 2.0
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="601f", MODE="0660", GROUP="plugdev", TAG+="uaccess"

View File

@@ -0,0 +1,204 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.limesdr;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.dsp.filter.channelizer.ComplexPolyphaseChannelizerM2;
import io.github.dsheirer.limesdr.DeviceEntry;
import io.github.dsheirer.limesdr.LogLevel;
import io.github.dsheirer.limesdr.Range;
import io.github.dsheirer.limesdr.StreamConfig;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerFactory;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Tests for the parts of the LimeSDR tuner that don't require a device.
*/
class LimeSdrTunerTest
{
private static final DeviceEntry USB_A = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00462008");
private static final DeviceEntry USB_B = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00471B22");
private static final DeviceEntry MINI = DeviceEntry.parse("LimeSDR-Mini, media=USB3.0, addr=0403:601f, serial=1D3AC9A5C7B4D2");
@Test
void lookupFindsEachUsbProduct()
{
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x1D50, (short)0x6108));
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x0403, (short)0x601F));
assertTrue(TunerClass.LIMESDR.isSupportedUsbTuner());
assertTrue(TunerType.LIMESDR.isSupportedUsbTuner());
}
@Test
void scalesSamplesAndRemovesHalfBitOffset()
{
short[] samples = {2047, -2048, -1, 0};
assertArrayEquals(new float[]{2047.5f / 2048, -2047.5f / 2048, -0.5f / 2048, 0.5f / 2048},
LimeSdrTunerController.scale(samples));
}
@Test
void sampleRatesAreLimitedByTheDevice()
{
//LimeSDR-USB on USB 3.0
assertEquals(List.of(LimeSdrSampleRate.values()), LimeSdrSampleRate.getRates(100_000, 61_440_000));
//LimeSDR Mini
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getRates(100_000, 30_720_000).getLast());
}
@Test
void usb2LimitsTheSampleRate()
{
Range range = new Range(100_000, 61_440_000, 0);
assertEquals(61_440_000, LimeSdrTunerController.getMaximumSampleRate(range, "USB3.0"));
double usb2 = LimeSdrTunerController.getMaximumSampleRate(range, "USB2.0");
assertEquals(LimeSdrSampleRate.RATE_10_0, LimeSdrSampleRate.getRates(range.min(), usb2).getLast());
}
@Test
void savedRateAboveTheDeviceMaximumIsReduced()
{
List<LimeSdrSampleRate> rates = LimeSdrSampleRate.getRates(100_000, 30_720_000);
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getClosest(rates, LimeSdrSampleRate.RATE_61_44.getRate()));
assertEquals(LimeSdrSampleRate.RATE_1_25, LimeSdrSampleRate.getClosest(rates, 1));
}
@Test
void sampleRatesDivideIntoPolyphaseChannels()
{
for(LimeSdrSampleRate rate : LimeSdrSampleRate.values())
{
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(rate.getRate());
assertEquals(0, channelCount % 2, rate.toString());
assertTrue((double)rate.getRate() / channelCount >= 25_000.0, rate.toString());
}
}
@Test
void maximumUsbRateDoesNotOverflow()
{
assertTrue(LimeSdrSampleRate.RATE_61_44.getRate() * 24L <= Integer.MAX_VALUE);
}
@Test
void antennaListExcludesNoneAndLoopbackPorts()
{
List<String> ports = List.of("None", "LNAH", "LNAL", "LNAW", "LB1", "LB2");
List<String> antennas = LimeSdrTunerController.getSelectableAntennas(ports);
assertEquals(List.of("LNAH", "LNAL", "LNAW"), antennas);
assertEquals("LNAW", LimeSdrTunerController.getDefaultAntenna(antennas));
assertEquals("LNAH", LimeSdrTunerController.getDefaultAntenna(List.of("LNAH")));
}
@Test
void deviceIsFoundBySerialNumber() throws SourceException
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00471B22");
assertEquals(USB_B, LimeSdrTunerController.findDevice(identity, List.of(MINI, USB_A, USB_B)));
}
@Test
void deviceWithoutSerialIsFoundWhenItIsTheOnlyOneOfItsKind() throws SourceException
{
LimeSdrTunerController.UsbIdentity mini = new LimeSdrTunerController.UsbIdentity(0x0403, 0x601F, null);
LimeSdrTunerController.UsbIdentity usb = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108, null);
assertEquals(MINI, LimeSdrTunerController.findDevice(mini, List.of(USB_A, MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(usb, List.of(USB_A, USB_B, MINI)));
}
@Test
void deviceThatLimeSuiteCantSeeIsAnError()
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00462008");
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(USB_B)));
}
@Test
void configurationSurvivesSerialization() throws Exception
{
LimeSdrTunerConfiguration configuration = new LimeSdrTunerConfiguration("test");
configuration.setSampleRate(LimeSdrSampleRate.RATE_5_0.getRate());
configuration.setGain(55);
configuration.setAntenna("LNAH");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = mapper.writeValueAsString(configuration);
TunerConfiguration restored = mapper.readValue(json, TunerConfiguration.class);
LimeSdrTunerConfiguration limeSdr = assertInstanceOf(LimeSdrTunerConfiguration.class, restored);
assertEquals(TunerType.LIMESDR, limeSdr.getTunerType());
assertEquals(LimeSdrSampleRate.RATE_5_0.getRate(), limeSdr.getSampleRate());
assertEquals(55, limeSdr.getGain());
assertEquals("LNAH", limeSdr.getAntenna());
assertEquals("test", limeSdr.getUniqueID());
}
@Test
void factoryCreatesTheConfiguration()
{
assertInstanceOf(LimeSdrTunerConfiguration.class, TunerFactory.getTunerConfiguration(TunerType.LIMESDR, "id"));
}
@Test
void fifoHoldsAQuarterSecondOfSamples()
{
assertEquals(StreamConfig.DEFAULT_FIFO_SIZE, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_1_25));
assertEquals(10_000_000, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_40_0));
}
@Test
void debugMessagesAreDiscarded()
{
LimeSdrTunerController.log(LogLevel.DEBUG, "Loss: pkt:4622555 exp: 000000017a863dc0, got: 000000017a864860");
LimeSdrTunerController.log(LogLevel.VERBOSE, "Set Rx LO frequency (450100180 Hz)");
}
@Test
void droppedSamplesAreFoundFromTimestampGaps()
{
assertEquals(0, LimeSdrTunerController.getTimestampGap(-1, 5_000_000));
assertEquals(0, LimeSdrTunerController.getTimestampGap(65536, 65536));
assertEquals(718_080, LimeSdrTunerController.getTimestampGap(32_309_248, 33_027_328));
assertEquals(0, LimeSdrTunerController.getTimestampGap(32_309_248, 0));
}
}