#2542 bladeRF SDR tuner support (#2543)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-31 08:24:45 -04:00
committed by GitHub
parent e65ff5a8dc
commit 276f4a316a
14 changed files with 1678 additions and 7 deletions

View File

@@ -99,6 +99,7 @@ dependencies {
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.22.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhydrasdr:1.1.3' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs

View File

@@ -16,5 +16,13 @@ dependencyResolutionManagement {
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
}
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jbladerf' }
}
}
}

View File

@@ -27,6 +27,7 @@ public enum TunerClass
{
AIRSPY("Airspy"),
AIRSPY_HF("Airspy HF+"),
BLADERF("bladeRF"),
FUNCUBE_DONGLE_PRO("Funcube Dongle Pro" ),
FUNCUBE_DONGLE_PRO_PLUS("Funcube Dongle Pro+" ),
HACKRF("HackRF" ),
@@ -37,7 +38,7 @@ public enum TunerClass
RECORDING_TUNER("Recording"),
UNKNOWN("Unknown" );
private String mDescription;
private final String mDescription;
/**
* Constructs an entry
@@ -54,8 +55,8 @@ public enum TunerClass
return mDescription;
}
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, HACKRF, HYDRASDR,
RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, BLADERF, HACKRF,
HYDRASDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerClass> FUNCUBE_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
@@ -122,6 +123,10 @@ public enum TunerClass
case 0x1D50CC15: //HACKRF ONE
case 0x1D50604B: //HACKRF JAWBREAKER
return HACKRF;
case 0x2CF05246: //BLADERF X40/X115
case 0x1D506066: //BLADERF X40/X115 LEGACY VID/PID
case 0x2CF05250: //BLADERF 2.0 MICRO
return BLADERF;
case 0x38AF0001: //HYDRASDR RFONE (official VID 38af, PID 0001)
return HYDRASDR;
case 0x1D5060A1:
@@ -129,7 +134,7 @@ public enum TunerClass
case 0x03EB800C:
return AIRSPY_HF;
}
return TunerClass.UNKNOWN;
}
}

View File

@@ -32,6 +32,10 @@ import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTuner;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerConfiguration;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerController;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerEditor;
import io.github.dsheirer.source.tuner.bladerf.BladeRfTuner;
import io.github.dsheirer.source.tuner.bladerf.BladeRfTunerConfiguration;
import io.github.dsheirer.source.tuner.bladerf.BladeRfTunerController;
import io.github.dsheirer.source.tuner.bladerf.BladeRfTunerEditor;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.fcd.FCDTuner;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerConfiguration;
@@ -361,6 +365,8 @@ public class TunerFactory
return new AirspyTuner(new AirspyTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case AIRSPY_HF:
return new AirspyHfTuner(new AirspyHfTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case BLADERF:
return new BladeRfTuner(new BladeRfTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case FUNCUBE_DONGLE_PRO:
TargetDataLine tdl1 = MixerManager.getTunerTargetDataLine(MixerTunerType.FUNCUBE_DONGLE_PRO);
if(tdl1 != null)
@@ -418,6 +424,8 @@ public class TunerFactory
return new AirspyHfTunerConfiguration(uniqueID);
case AIRSPY_R820T:
return new AirspyTunerConfiguration(uniqueID);
case BLADERF:
return new BladeRfTunerConfiguration(uniqueID);
case ELONICS_E4000:
return new E4KTunerConfiguration(uniqueID);
case FITIPOWER_FC0013:
@@ -473,6 +481,8 @@ public class TunerFactory
return new FCD1TunerEditor(userPreferences, tunerManager, discoveredTuner);
case FUNCUBE_DONGLE_PRO_PLUS:
return new FCD2TunerEditor(userPreferences, tunerManager, discoveredTuner);
case BLADERF:
return new BladeRfTunerEditor(userPreferences, tunerManager, discoveredTuner);
case HACKRF:
return new HackRFTunerEditor(userPreferences, tunerManager, discoveredTuner);
case HYDRASDR:

View File

@@ -26,6 +26,7 @@ import java.util.EnumSet;
public enum TunerType
{
AIRSPY_R820T("R820T"),
BLADERF("bladeRF"),
AIRSPY_HF_PLUS("HF+"),
ELONICS_E4000("E4000"),
FCI_FC2580("FC2580"),
@@ -51,7 +52,7 @@ public enum TunerType
RECORDING("Recording"),
UNKNOWN("Unknown");
private String mLabel;
private final String mLabel;
/**
* Constructs an instance
@@ -73,13 +74,13 @@ public enum TunerType
/**
* Supported USB tuner types
*/
public static EnumSet<TunerType> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY_R820T, ELONICS_E4000, FITIPOWER_FC0012,
public static final EnumSet<TunerType> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY_R820T, BLADERF, ELONICS_E4000, FITIPOWER_FC0012,
HACKRF_JAWBREAKER, HACKRF_RAD1O, HACKRF_ONE, HYDRASDR_R828D, RAFAELMICRO_R820T);
/**
* Supported sound card tuner types
*/
public static EnumSet<TunerType> SUPPORTED_SOUND_CARD_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerType> SUPPORTED_SOUND_CARD_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
/**
* Indicates if this tuner is supported as a USB tuner

View File

@@ -0,0 +1,33 @@
/*
* *****************************************************************************
* 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.bladerf;
/**
* bladeRF device information, formatted for display.
*
* @param serial number
* @param board name reported by the library
* @param firmware version
* @param fpga version
* @param speed of the USB connection
*/
public record BladeRfDeviceInformation(String serial, String board, String firmware, String fpga, String speed)
{
}

View File

@@ -0,0 +1,105 @@
/*
* *****************************************************************************
* 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.bladerf;
import java.util.ArrayList;
import java.util.List;
/**
* Sample rates offered for bladeRF tuners. Each is a multiple of the 25 kHz polyphase channel spacing.
*/
public enum BladeRfSampleRate
{
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");
public static final BladeRfSampleRate DEFAULT = RATE_10_0;
private final int mRate;
private final String mLabel;
BladeRfSampleRate(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 maximum, which is the lower of the hardware limit and what the USB link sustains.
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/
public static List<BladeRfSampleRate> getRates(long maximumRate)
{
List<BladeRfSampleRate> rates = new ArrayList<>();
for(BladeRfSampleRate rate : values())
{
if(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 BladeRfSampleRate getClosest(List<BladeRfSampleRate> rates, int requested)
{
BladeRfSampleRate closest = rates.getFirst();
for(BladeRfSampleRate rate : rates)
{
if(rate.getRate() <= requested)
{
closest = rate;
}
}
return closest;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.bladerf;
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;
/**
* bladeRF tuner
*/
public class BladeRfTuner extends Tuner
{
private static final int BITS_PER_SAMPLE = 32;
/**
* Constructs an instance
* @param controller for the bladeRF
* @param tunerErrorListener to listen for errors from this tuner
* @param channelizerType for the channelizer
*/
public BladeRfTuner(BladeRfTunerController controller, ITunerErrorListener tunerErrorListener,
ChannelizerType channelizerType)
{
super(controller, tunerErrorListener, channelizerType);
}
/**
* bladeRF tuner controller
*/
public BladeRfTunerController getController()
{
return (BladeRfTunerController)getTunerController();
}
@Override
public String getPreferredName()
{
return "bladeRF " + getUniqueID();
}
@Override
public String getUniqueID()
{
return getController().getDeviceInfo().serial();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.BLADERF;
}
@Override
public double getSampleSize()
{
return 12.0;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return BladeRfSampleRate.RATE_30_0.getRate() * BITS_PER_SAMPLE;
}
}

View File

@@ -0,0 +1,104 @@
/*
* *****************************************************************************
* 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.bladerf;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
/**
* bladeRF tuner configuration.
*/
public class BladeRfTunerConfiguration extends TunerConfiguration
{
private int mSampleRate = BladeRfSampleRate.DEFAULT.getRate();
private GainMode mGainMode = BladeRfTunerController.GAIN_MODE_DEFAULT;
private int mGain = BladeRfTunerController.GAIN_DEFAULT;
private boolean mBiasT = false;
/**
* Default constructor for Jackson
*/
public BladeRfTunerConfiguration()
{
super(BladeRfTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, BladeRfTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ);
}
/**
* Constructs an instance
* @param uniqueID for the tuner
*/
public BladeRfTunerConfiguration(String uniqueID)
{
super(uniqueID);
}
@Override
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public TunerType getTunerType()
{
return TunerType.BLADERF;
}
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
@JacksonXmlProperty(isAttribute = true, localName = "gain_mode")
public GainMode getGainMode()
{
return mGainMode;
}
public void setGainMode(GainMode gainMode)
{
mGainMode = gainMode;
}
@JacksonXmlProperty(isAttribute = true, localName = "gain")
public int getGain()
{
return mGain;
}
public void setGain(int gain)
{
mGain = gain;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -0,0 +1,775 @@
/*
* *****************************************************************************
* 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.bladerf;
import io.github.dsheirer.bladerf.BladeRf;
import io.github.dsheirer.bladerf.BladeRfDevice;
import io.github.dsheirer.bladerf.BladeRfError;
import io.github.dsheirer.bladerf.BladeRfException;
import io.github.dsheirer.bladerf.BladeRfStream;
import io.github.dsheirer.bladerf.Channel;
import io.github.dsheirer.bladerf.ChannelLayout;
import io.github.dsheirer.bladerf.DeviceInfo;
import io.github.dsheirer.bladerf.DeviceSpeed;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.bladerf.GainModeInfo;
import io.github.dsheirer.bladerf.Range;
import io.github.dsheirer.bladerf.SampleBuffer;
import io.github.dsheirer.bladerf.SampleFormat;
import io.github.dsheirer.bladerf.StreamConfig;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.sample.Listener;
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;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.Context;
import org.usb4java.Device;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
/**
* bladeRF tuner controller, implemented with the jBladeRF library (libbladeRF).
*
* The tuner is discovered with LibUsb at a USB bus and port, like all other USB tuners, however libbladeRF owns the USB
* device and its transfers.
*
* Only the first receive channel (RX1 connector) is used. The bladeRF 2.0 has a second receive channel, but it shares
* the local oscillator and sample rate with the first, so it couldn't be tuned as an independent tuner.
*/
public class BladeRfTunerController extends TunerController
{
private static final Logger mLog = LoggerFactory.getLogger(BladeRfTunerController.class);
//The bladeRF 2.0 range. The device reports its own range when it's started.
public static final long MINIMUM_TUNABLE_FREQUENCY_HZ = 70_000_000L;
public static final long MAXIMUM_TUNABLE_FREQUENCY_HZ = 6_000_000_000L;
public static final GainMode GAIN_MODE_DEFAULT = GainMode.MANUAL;
public static final int GAIN_DEFAULT = 30;
private static final long FREQUENCY_DEFAULT = 101_100_000L;
private static final Channel CHANNEL = Channel.RX0;
private static final float SAMPLE_SCALE = 1.0f / 2048.0f;
private static final SampleFormat SAMPLE_FORMAT = SampleFormat.SC16_Q11;
private static final int BUFFER_SAMPLE_COUNT = 65536;
private static final StreamConfig STREAM_CONFIG = new StreamConfig(32, BUFFER_SAMPLE_COUNT, 16);
private static final long USB2_MAXIMUM_BYTES_PER_SECOND = 32_000_000L;
private static final double MAXIMUM_USABLE_BANDWIDTH_PERCENT = 0.90;
private static final long STREAM_CHECK_INTERVAL_SECONDS = 1;
private final int mBus;
private final String mPortAddress;
private BladeRfDevice mDevice;
private BladeRfDeviceInformation mDeviceInfo;
private BladeRfStream mStream;
private ScheduledFuture<?> mStreamCheck;
private List<BladeRfSampleRate> mSampleRates = List.of();
private BladeRfSampleRate mSampleRate;
private List<GainModeInfo> mGainModes = List.of();
private Range mGainRange;
private long mMinimumTunableFrequency = MINIMUM_TUNABLE_FREQUENCY_HZ;
private long mMaximumTunableFrequency = MAXIMUM_TUNABLE_FREQUENCY_HZ;
private GainMode mGainMode = GAIN_MODE_DEFAULT;
private int mGain = GAIN_DEFAULT;
private boolean mBiasTSupported;
private boolean mBiasT;
private volatile float mSamplesPerMillisecond = 0.0f;
/**
* Constructs an instance
* @param bus usb
* @param portAddress usb
* @param tunerErrorListener to receive error notifications from this controller
*/
public BladeRfTunerController(int bus, String portAddress, ITunerErrorListener tunerErrorListener)
{
super(tunerErrorListener);
mBus = bus;
mPortAddress = portAddress;
setMinimumFrequency(MINIMUM_TUNABLE_FREQUENCY_HZ);
setMaximumFrequency(MAXIMUM_TUNABLE_FREQUENCY_HZ);
setMiddleUnusableHalfBandwidth(0);
setUsableBandwidthPercentage(MAXIMUM_USABLE_BANDWIDTH_PERCENT);
}
@Override
public TunerType getTunerType()
{
return TunerType.BLADERF;
}
@Override
public int getBufferSampleCount()
{
return BUFFER_SAMPLE_COUNT;
}
@Override
public void start() throws SourceException
{
getLock().lock();
try
{
mDevice = BladeRf.open(findDevice());
if(!mDevice.isFpgaConfigured())
{
throw new SourceException("bladeRF FPGA isn't loaded - use bladeRF-cli to flash an FPGA image to the device");
}
mBiasTSupported = isBiasTSupported(mDevice);
mDeviceInfo = new BladeRfDeviceInformation(mDevice.serial(), mDevice.boardName(),
mDevice.firmwareVersion().toString(), mDevice.fpgaVersion().toString(), mDevice.speed().name());
mSampleRates = BladeRfSampleRate.getRates(getMaximumSampleRate(mDevice));
Range frequencies = mDevice.frequencyRange(CHANNEL);
mMinimumTunableFrequency = frequencies.min();
mMaximumTunableFrequency = frequencies.max();
setFrequencyExtents(mMinimumTunableFrequency, mMaximumTunableFrequency);
mGainModes = mDevice.gainModes(CHANNEL);
setSampleRate(BladeRfSampleRate.getClosest(mSampleRates, BladeRfSampleRate.DEFAULT.getRate()));
setFrequency(FREQUENCY_DEFAULT);
setGainMode(GAIN_MODE_DEFAULT);
setGain(GAIN_DEFAULT);
mGainRange = mDevice.gainRange(CHANNEL);
}
catch(BladeRfException | LinkageError e)
{
closeDevice(mDevice, false);
mDevice = null;
throw new SourceException("Can't start bladeRF tuner - " + e.getMessage(), e);
}
catch(SourceException se)
{
closeDevice(mDevice, false);
mDevice = null;
throw se;
}
finally
{
getLock().unlock();
}
}
@Override
public void stop()
{
BladeRfDevice device;
BladeRfStream stream;
boolean biasT;
getLock().lock();
try
{
device = mDevice;
mDevice = null;
stream = clearStream();
biasT = mBiasT;
mBiasT = false;
}
finally
{
getLock().unlock();
}
mNativeBufferBroadcaster.clear();
if(stream != null)
{
stream.stop();
}
closeDevice(device, biasT);
}
/**
* Closes the device. The bias-T stays on after the device is closed, so it has to be turned off first.
* @param device to close, or null
* @param biasT true if the bias-T is on
*/
private static void closeDevice(BladeRfDevice device, boolean biasT)
{
if(device != null)
{
try
{
if(biasT)
{
device.setBiasTee(CHANNEL, false);
}
}
catch(BladeRfException | RuntimeException e)
{
mLog.error("Error turning off bladeRF bias-T", e);
}
try
{
device.close();
}
catch(RuntimeException e)
{
mLog.error("Error closing bladeRF device", e);
}
}
}
/**
* Probes for bias-T support, since only the bladeRF 2.0 has a bias-T.
*/
private static boolean isBiasTSupported(BladeRfDevice device) throws BladeRfException
{
try
{
device.biasTee(CHANNEL);
return true;
}
catch(BladeRfException e)
{
if(e.error() == BladeRfError.UNSUPPORTED)
{
return false;
}
throw e;
}
}
/**
* Highest sample rate supported by the hardware and the USB connection.
*/
private static long getMaximumSampleRate(BladeRfDevice device) throws BladeRfException
{
long maximum = device.sampleRateRange(CHANNEL).max();
if(device.speed() != DeviceSpeed.SUPER)
{
maximum = Math.min(maximum, USB2_MAXIMUM_BYTES_PER_SECOND / SAMPLE_FORMAT.bytesPerSample());
}
return maximum;
}
/**
* Finds the libbladeRF description of the device at this controller's USB bus and port.
*/
private DeviceInfo findDevice() throws SourceException, BladeRfException
{
int address = readUsbAddress();
for(DeviceInfo info : BladeRf.listDevices())
{
if(info.usbBus() == mBus && info.usbAddress() == address)
{
return info;
}
}
throw new SourceException("bladeRF library can't access the USB device at bus [" + mBus + "] port [" +
mPortAddress + "] - if using linux, check the udev rules. If using windows, install the WinUSB driver.");
}
/**
* Reads the current USB address of the device at this controller's USB bus and port. The address changes each time
* that the device is plugged in.
*/
private int readUsbAddress() throws SourceException
{
Context context = new Context();
int status = LibUsb.init(context);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
try
{
DeviceList deviceList = new DeviceList();
int count = LibUsb.getDeviceList(context, deviceList);
if(count < 0)
{
throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
}
try
{
for(Device device : deviceList)
{
if(LibUsb.getBusNumber(device) == mBus && LibUsb.getPortNumber(device) > 0 &&
mPortAddress != null && mPortAddress.equals(TunerManager.getPortAddress(device)))
{
return LibUsb.getDeviceAddress(device);
}
}
}
finally
{
LibUsb.freeDeviceList(deviceList, true);
}
}
finally
{
LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
}
@Override
public void apply(TunerConfiguration tunerConfiguration) throws SourceException
{
if(!(tunerConfiguration instanceof BladeRfTunerConfiguration config))
{
throw new IllegalArgumentException("Invalid tuner config:" + tunerConfiguration.getClass());
}
//Saved limits can be wider than this device, since the defaults are for the bladeRF 2.0.
config.setMinimumFrequency(Math.max(config.getMinimumFrequency(), mMinimumTunableFrequency));
config.setMaximumFrequency(Math.min(config.getMaximumFrequency() > 0 ? config.getMaximumFrequency() :
mMaximumTunableFrequency, mMaximumTunableFrequency));
BladeRfSampleRate rate = BladeRfSampleRate.getClosest(mSampleRates, config.getSampleRate());
setSampleRate(rate);
config.setSampleRate(rate.getRate());
//Invoke super for frequency, frequency correction and autoPPM
super.apply(config);
setBiasT(config.isBiasT());
setGain(config.getGain());
setGainMode(isSupported(config.getGainMode()) ? config.getGainMode() : GAIN_MODE_DEFAULT);
}
private boolean isSupported(GainMode mode)
{
return mGainModes.stream().anyMatch(info -> info.mode() == mode);
}
@Override
public long getTunedFrequency() throws SourceException
{
return mFrequencyController.getTunedFrequency();
}
@Override
public void setTunedFrequency(long frequency) throws SourceException
{
getLock().lock();
try
{
if(mDevice != null)
{
mDevice.setFrequency(CHANNEL, frequency);
}
}
catch(BladeRfException e)
{
throw new SourceException("Can't tune bladeRF to [" + frequency + "] - " + e.getMessage(), e);
}
finally
{
getLock().unlock();
}
}
@Override
public double getCurrentSampleRate()
{
return mSampleRate != null ? mSampleRate.getRate() : 0;
}
/**
* Sample rates supported by the device across its current USB connection.
*/
public List<BladeRfSampleRate> getSampleRates()
{
return mSampleRates;
}
/**
* Current sample rate.
*/
public BladeRfSampleRate getBladeRfSampleRate()
{
return mSampleRate;
}
/**
* Sets the sample rate and the matching analog filter bandwidth. The sample stream is stopped for the change.
* @param rate to set
* @throws SourceException if the sample rate is locked or can't be set
*/
public void setSampleRate(BladeRfSampleRate rate) throws SourceException
{
getLock().lock();
try
{
if(rate != mSampleRate && mDevice != null)
{
if(isLockedSampleRate())
{
throw new SourceException("Can't change bladeRF sample rate while channels are allocated");
}
boolean streaming = mStream != null;
BladeRfStream stream = clearStream();
if(stream != null)
{
stream.stop();
}
try
{
mDevice.setSampleRate(CHANNEL, rate.getRate());
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
(double)bandwidth / rate.getRate()));
mSampleRate = rate;
mSamplesPerMillisecond = rate.getRate() / 1000.0f;
mFrequencyController.setSampleRate(rate.getRate());
}
catch(BladeRfException e)
{
throw new SourceException("Can't set bladeRF sample rate to [" + rate + "] - " + e.getMessage(), e);
}
finally
{
if(streaming)
{
startStream();
}
}
}
}
finally
{
getLock().unlock();
}
}
/**
* Gain modes supported by the device.
*/
public List<GainModeInfo> getGainModes()
{
return mGainModes;
}
/**
* Range of gain values, available once the tuner has been started.
*/
public Range getGainRange()
{
return mGainRange;
}
/**
* Lowest frequency that the device can tune.
*/
public long getMinimumTunableFrequency()
{
return mMinimumTunableFrequency;
}
/**
* Highest frequency that the device can tune.
*/
public long getMaximumTunableFrequency()
{
return mMaximumTunableFrequency;
}
/**
* Sets the gain mode.
* @param mode to set
* @throws SourceException if the mode can't be set
*/
public void setGainMode(GainMode mode) throws SourceException
{
getLock().lock();
try
{
if(mDevice != null)
{
mDevice.setGainMode(CHANNEL, mode);
if(mode == GainMode.MANUAL)
{
mDevice.setGain(CHANNEL, mGain);
}
}
mGainMode = mode;
}
catch(BladeRfException e)
{
throw new SourceException("Can't set bladeRF gain mode to [" + mode + "] - " + e.getMessage(), e);
}
finally
{
getLock().unlock();
}
}
/**
* Sets the gain, which is used when the gain mode is manual.
* @param gain in dB
* @throws SourceException if the gain can't be set
*/
public void setGain(int gain) throws SourceException
{
getLock().lock();
try
{
mGain = gain;
if(mDevice != null && mGainMode == GainMode.MANUAL)
{
mDevice.setGain(CHANNEL, gain);
}
}
catch(BladeRfException e)
{
throw new SourceException("Can't set bladeRF gain to [" + gain + "] - " + e.getMessage(), e);
}
finally
{
getLock().unlock();
}
}
/**
* Indicates if the device has a bias-T.
*/
public boolean isBiasTSupported()
{
return mBiasTSupported;
}
/**
* Turns the bias-T on or off. On the bladeRF 2.0 this switches both receive connectors.
* @param enabled true to turn on
* @throws SourceException if the bias-T can't be set
*/
public void setBiasT(boolean enabled) throws SourceException
{
getLock().lock();
try
{
if(mDevice != null && mBiasTSupported)
{
mDevice.setBiasTee(CHANNEL, enabled);
mBiasT = enabled;
}
}
catch(BladeRfException e)
{
throw new SourceException("Can't set bladeRF bias-T - " + e.getMessage(), e);
}
finally
{
getLock().unlock();
}
}
/**
* Device information, available once the tuner has been started.
*/
public BladeRfDeviceInformation getDeviceInfo()
{
return mDeviceInfo;
}
/**
* Adds the IQ buffer listener and automatically starts streaming, if not already started.
*/
@Override
public void addBufferListener(Listener<INativeBuffer> listener)
{
getLock().lock();
try
{
if(mDevice != null)
{
super.addBufferListener(listener);
if(mStream == null)
{
startStream();
}
}
}
catch(SourceException se)
{
mLog.error("Error starting streaming on bladeRF tuner", se);
}
finally
{
getLock().unlock();
}
}
/**
* Removes the IQ buffer listener and stops streaming if there are no more listeners.
*/
@Override
public void removeBufferListener(Listener<INativeBuffer> listener)
{
getLock().lock();
try
{
super.removeBufferListener(listener);
if(!hasBufferListeners())
{
BladeRfStream stream = clearStream();
if(stream != null)
{
stream.stop();
}
}
}
finally
{
getLock().unlock();
}
}
/**
* Starts the sample stream.
*/
private void startStream() throws SourceException
{
try
{
mStream = mDevice.startRx(ChannelLayout.RX_X1, SAMPLE_FORMAT, STREAM_CONFIG, this::receive);
mStreamCheck = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::checkStream, STREAM_CHECK_INTERVAL_SECONDS,
STREAM_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
catch(BladeRfException | RuntimeException e)
{
throw new SourceException("Can't start bladeRF sample stream - " + e.getMessage(), e);
}
}
/**
* Clears the sample stream so that it's no longer monitored. The caller is responsible for stopping it.
* @return stream or null if there is no stream
*/
private BladeRfStream clearStream()
{
if(mStreamCheck != null)
{
mStreamCheck.cancel(false);
mStreamCheck = null;
}
BladeRfStream stream = mStream;
mStream = null;
return stream;
}
/**
* Detects a stream that ended by itself, such as when the device has an error, and flags the tuner.
*/
private void checkStream()
{
String message = null;
getLock().lock();
try
{
if(mStream != null && !mStream.isRunning())
{
message = "bladeRF sample stream stopped - " + mStream.failure();
clearStream();
}
}
finally
{
getLock().unlock();
}
//Outside of the lock because this stops the tuner
if(message != null)
{
mLog.error(message);
setErrorMessage(message);
}
}
/**
* Receives samples on the stream thread and dispatches them to the buffer listeners. Exceptions are contained here
* since an exception reaching the library stops the sample stream.
* @param buffer from the library, which is only valid until this method returns.
* @return true to continue streaming
*/
private boolean receive(SampleBuffer buffer)
{
try
{
broadcast(new FloatNativeBuffer(scale(buffer.copyShorts()), System.currentTimeMillis(), mSamplesPerMillisecond));
}
catch(RuntimeException e)
{
mLog.error("Error dispatching bladeRF sample buffer", e);
}
return true;
}
/**
* Scales 12-bit samples to the range -1 to 1.
* @param samples interleaved complex samples
* @return scaled interleaved complex samples
*/
static float[] scale(short[] samples)
{
float[] scaled = new float[samples.length];
for(int x = 0; x < samples.length; x++)
{
scaled[x] = samples[x] * SAMPLE_SCALE;
}
return scaled;
}
}

View File

@@ -0,0 +1,437 @@
/*
* *****************************************************************************
* 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.bladerf;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.bladerf.GainModeInfo;
import io.github.dsheirer.bladerf.Range;
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.awt.Component;
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.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.SpinnerNumberModel;
/**
* bladeRF tuner editor
*/
public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerConfiguration>
{
@Serial
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(BladeRfTunerEditor.class);
private JButton mTunerInfoButton;
private JComboBox<BladeRfSampleRate> mSampleRateCombo;
private JCheckBox mBiasTCheckBox;
private JComboBox<GainModeInfo> mGainModeCombo;
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 bladeRF tuner and controller
*/
public BladeRfTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
{
super(userPreferences, tunerManager, discoveredTuner);
init();
tunerStatusUpdated();
}
@Override
public long getMinimumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMinimumTunableFrequency() :
BladeRfTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ;
}
@Override
public long getMaximumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMaximumTunableFrequency() :
BladeRfTunerController.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,push");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel());
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Gain Control"), "wrap");
add(new JLabel("Mode:"));
add(getGainModeCombo(), "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
}
@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)
{
BladeRfTunerController controller = getTuner().getController();
List<BladeRfSampleRate> rates = controller.getSampleRates();
getSampleRateCombo().setModel(new DefaultComboBoxModel<>(rates.toArray(new BladeRfSampleRate[0])));
getSampleRateCombo().setSelectedItem(controller.getBladeRfSampleRate());
getSampleRateCombo().setEnabled(!controller.isLockedSampleRate());
getBiasTCheckBox().setVisible(controller.isBiasTSupported());
getBiasTCheckBox().setEnabled(true);
List<GainModeInfo> modes = controller.getGainModes();
getGainModeCombo().setModel(new DefaultComboBoxModel<>(modes.toArray(new GainModeInfo[0])));
getGainModeCombo().setEnabled(true);
Range range = controller.getGainRange();
getGainSlider().setMinimum((int)range.min());
getGainSlider().setMaximum((int)range.max());
if(hasConfiguration())
{
getBiasTCheckBox().setSelected(getConfiguration().isBiasT());
getGainSlider().setValue(getConfiguration().getGain());
for(GainModeInfo mode : modes)
{
if(mode.mode() == getConfiguration().getGainMode())
{
getGainModeCombo().setSelectedItem(mode);
}
}
}
updateGainControls();
}
else
{
getSampleRateCombo().setModel(new DefaultComboBoxModel<>());
getSampleRateCombo().setEnabled(false);
getBiasTCheckBox().setEnabled(false);
getBiasTCheckBox().setSelected(false);
getGainModeCombo().setModel(new DefaultComboBoxModel<>());
getGainModeCombo().setEnabled(false);
getGainSlider().setEnabled(false);
getGainValueLabel().setEnabled(false);
}
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().getGainRange() != null;
}
/**
* Enables the gain slider when the gain mode is manual and refreshes the gain value label.
*/
private void updateGainControls()
{
GainModeInfo mode = (GainModeInfo)getGainModeCombo().getSelectedItem();
boolean manual = hasTuner() && mode != null && mode.mode() == GainMode.MANUAL;
getGainSlider().setEnabled(manual);
getGainValueLabel().setEnabled(manual);
getGainValueLabel().setText(manual ? getGainSlider().getValue() + " dB" : "AGC");
}
/**
* Button that provides tuner information
*/
private JButton getTunerInfoButton()
{
if(mTunerInfoButton == null)
{
mTunerInfoButton = new JButton("Info");
mTunerInfoButton.setEnabled(false);
mTunerInfoButton.addActionListener(_ -> JOptionPane.showMessageDialog(BladeRfTunerEditor.this,
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
}
return mTunerInfoButton;
}
private JComboBox<BladeRfSampleRate> getSampleRateCombo()
{
if(mSampleRateCombo == null)
{
mSampleRateCombo = new JComboBox<>();
mSampleRateCombo.setEnabled(false);
mSampleRateCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
BladeRfSampleRate rate = (BladeRfSampleRate)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 bladeRF sample rate", e1);
JOptionPane.showMessageDialog(BladeRfTunerEditor.this, "Couldn't set sample rate to " + rate);
}
}
});
}
return mSampleRateCombo;
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
private JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas. Also powers the RX2 connector.");
mBiasTCheckBox.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setBiasT(mBiasTCheckBox.isSelected());
save();
}
catch(Exception e1)
{
mLog.error("Error setting Bias-T", e1);
JOptionPane.showMessageDialog(mBiasTCheckBox, "Couldn't set Bias-T: " + e1.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
private JComboBox<GainModeInfo> getGainModeCombo()
{
if(mGainModeCombo == null)
{
mGainModeCombo = new JComboBox<>();
mGainModeCombo.setEnabled(false);
mGainModeCombo.setRenderer(new DefaultListCellRenderer()
{
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected,
boolean focused)
{
Object label = value instanceof GainModeInfo mode ? mode.name() : value;
return super.getListCellRendererComponent(list, label, index, selected, focused);
}
});
mGainModeCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
GainModeInfo mode = (GainModeInfo)mGainModeCombo.getSelectedItem();
try
{
getTuner().getController().setGainMode(mode.mode());
updateGainControls();
save();
}
catch(Exception e1)
{
mLog.error("Error setting bladeRF gain mode", e1);
JOptionPane.showMessageDialog(mGainModeCombo, "Couldn't set gain mode to " + mode.name());
}
}
});
}
return mGainModeCombo;
}
private JSlider getGainSlider()
{
if(mGainSlider == null)
{
mGainSlider = new JSlider(JSlider.HORIZONTAL, 0, 60, BladeRfTunerController.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 bladeRF gain to:" + gain, e);
JOptionPane.showMessageDialog(mGainSlider, "Couldn't set gain value to " + gain);
}
}
updateGainControls();
});
}
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 BladeRfSampleRate rate)
{
getConfiguration().setSampleRate(rate.getRate());
}
if(getGainModeCombo().getSelectedItem() instanceof GainModeInfo mode)
{
getConfiguration().setGainMode(mode.mode());
}
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
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())
{
BladeRfDeviceInformation info = getTuner().getController().getDeviceInfo();
StringBuilder sb = new StringBuilder();
sb.append("<html><h3>bladeRF Tuner</h3>");
sb.append("<b>Serial: </b>").append(info.serial()).append("<br>");
sb.append("<b>Board: </b>").append(info.board()).append("<br>");
sb.append("<b>Firmware: </b>").append(info.firmware()).append("<br>");
sb.append("<b>FPGA: </b>").append(info.fpga()).append("<br>");
sb.append("<b>USB Speed: </b>").append(info.speed()).append("<br>");
return sb.toString();
}
return null;
}
}

View File

@@ -26,6 +26,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.airspy.AirspyTunerConfiguration;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerConfiguration;
import io.github.dsheirer.source.tuner.bladerf.BladeRfTunerConfiguration;
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;
@@ -44,6 +45,7 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
@JsonSubTypes({
@JsonSubTypes.Type(value = AirspyTunerConfiguration.class, name = "airspyTunerConfiguration"),
@JsonSubTypes.Type(value = AirspyHfTunerConfiguration.class, name = "airspyHfTunerConfiguration"),
@JsonSubTypes.Type(value = BladeRfTunerConfiguration.class, name = "bladeRfTunerConfiguration"),
@JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"),
@JsonSubTypes.Type(value = FC0013TunerConfiguration.class, name = "fc0013TunerConfiguration"),
@JsonSubTypes.Type(value = FCD1TunerConfiguration.class, name = "fcd1TunerConfiguration"),

View File

@@ -0,0 +1,9 @@
# Nuand bladeRF udev rules
# Grants access to the logged-in user (uaccess) and to the plugdev group.
# bladeRF 2.0 micro
SUBSYSTEM=="usb", ATTR{idVendor}=="2cf0", ATTR{idProduct}=="5250", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# bladeRF x40/x115
SUBSYSTEM=="usb", ATTR{idVendor}=="2cf0", ATTR{idProduct}=="5246", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# bladeRF x40/x115 with the legacy vendor and product ID
SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6066", MODE="0660", GROUP="plugdev", TAG+="uaccess"

View File

@@ -0,0 +1,98 @@
/*
* *****************************************************************************
* 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.bladerf;
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 com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.source.tuner.TunerClass;
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 bladeRF tuner that don't require a device.
*/
class BladeRfTunerTest
{
private static final float SCALE = 2048.0f;
@Test
void lookupFindsEachUsbProduct()
{
assertEquals(TunerClass.BLADERF, TunerClass.lookup((short)0x2CF0, (short)0x5250));
assertEquals(TunerClass.BLADERF, TunerClass.lookup((short)0x2CF0, (short)0x5246));
assertEquals(TunerClass.BLADERF, TunerClass.lookup((short)0x1D50, (short)0x6066));
}
@Test
void scalesSamples()
{
short[] samples = {2047, -2048, 1024, -1024};
assertArrayEquals(new float[]{2047 / SCALE, -1.0f, 0.5f, -0.5f}, BladeRfTunerController.scale(samples));
}
@Test
void sampleRatesAreLimitedByTheDevice()
{
List<BladeRfSampleRate> rates = BladeRfSampleRate.getRates(4_000_000);
assertEquals(List.of(BladeRfSampleRate.RATE_1_25, BladeRfSampleRate.RATE_2_5), rates);
assertEquals(BladeRfSampleRate.RATE_2_5, BladeRfSampleRate.getClosest(rates, BladeRfSampleRate.DEFAULT.getRate()));
assertEquals(BladeRfSampleRate.RATE_1_25, BladeRfSampleRate.getClosest(rates, 1));
}
@Test
void sampleRatesDivideIntoPolyphaseChannels()
{
for(BladeRfSampleRate rate : BladeRfSampleRate.values())
{
assertEquals(0, rate.getRate() % 50_000, rate.toString());
}
}
@Test
void configurationSurvivesSerialization() throws Exception
{
BladeRfTunerConfiguration configuration = new BladeRfTunerConfiguration("test");
configuration.setSampleRate(BladeRfSampleRate.RATE_5_0.getRate());
configuration.setGainMode(GainMode.SLOW_ATTACK_AGC);
configuration.setGain(12);
configuration.setBiasT(true);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = mapper.writeValueAsString(configuration);
TunerConfiguration restored = mapper.readValue(json, TunerConfiguration.class);
BladeRfTunerConfiguration bladeRf = assertInstanceOf(BladeRfTunerConfiguration.class, restored);
assertEquals(TunerType.BLADERF, bladeRf.getTunerType());
assertEquals(BladeRfSampleRate.RATE_5_0.getRate(), bladeRf.getSampleRate());
assertEquals(GainMode.SLOW_ATTACK_AGC, bladeRf.getGainMode());
assertEquals(12, bladeRf.getGain());
assertEquals(true, bladeRf.isBiasT());
assertEquals("test", bladeRf.getUniqueID());
}
}