From 276f4a316a0ce81c6f4e531f9127b247dfb9073e Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sat, 31 Oct 2026 08:24:45 -0400 Subject: [PATCH] #2542 bladeRF SDR tuner support (#2543) Co-authored-by: denny --- build.gradle | 1 + settings.gradle | 8 + .../dsheirer/source/tuner/TunerClass.java | 13 +- .../dsheirer/source/tuner/TunerFactory.java | 10 + .../dsheirer/source/tuner/TunerType.java | 7 +- .../bladerf/BladeRfDeviceInformation.java | 33 + .../tuner/bladerf/BladeRfSampleRate.java | 105 +++ .../source/tuner/bladerf/BladeRfTuner.java | 83 ++ .../bladerf/BladeRfTunerConfiguration.java | 104 +++ .../tuner/bladerf/BladeRfTunerController.java | 775 ++++++++++++++++++ .../tuner/bladerf/BladeRfTunerEditor.java | 437 ++++++++++ .../configuration/TunerConfiguration.java | 2 + src/main/resources/88-nuand-bladerf.rules | 9 + .../tuner/bladerf/BladeRfTunerTest.java | 98 +++ 14 files changed, 1678 insertions(+), 7 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfDeviceInformation.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfSampleRate.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTuner.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerConfiguration.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerEditor.java create mode 100644 src/main/resources/88-nuand-bladerf.rules create mode 100644 src/test/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerTest.java diff --git a/build.gradle b/build.gradle index 81021e5c..14d6c3cc 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/settings.gradle b/settings.gradle index e691aca6..f4985b54 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' } + } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java index ee0074b9..6af936cc 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java @@ -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 SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, HACKRF, HYDRASDR, - RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS); + public static final EnumSet SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, BLADERF, HACKRF, + HYDRASDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS); public static final EnumSet 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; } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java index 12639076..bb8b9e56 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java @@ -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: diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java index f2e10806..39b75201 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java @@ -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 SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY_R820T, ELONICS_E4000, FITIPOWER_FC0012, + public static final EnumSet 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 SUPPORTED_SOUND_CARD_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS); + public static final EnumSet SUPPORTED_SOUND_CARD_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS); /** * Indicates if this tuner is supported as a USB tuner diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfDeviceInformation.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfDeviceInformation.java new file mode 100644 index 00000000..e8a2a7cd --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfDeviceInformation.java @@ -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 + * **************************************************************************** + */ + +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) +{ +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfSampleRate.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfSampleRate.java new file mode 100644 index 00000000..8307a476 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfSampleRate.java @@ -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 + * **************************************************************************** + */ + +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 getRates(long maximumRate) + { + List 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 rates, int requested) + { + BladeRfSampleRate closest = rates.getFirst(); + + for(BladeRfSampleRate rate : rates) + { + if(rate.getRate() <= requested) + { + closest = rate; + } + } + + return closest; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTuner.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTuner.java new file mode 100644 index 00000000..1ffcbf1c --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTuner.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerConfiguration.java new file mode 100644 index 00000000..aa380c6f --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerConfiguration.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java new file mode 100644 index 00000000..2a5354e6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java @@ -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 + * **************************************************************************** + */ + +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 mSampleRates = List.of(); + private BladeRfSampleRate mSampleRate; + private List 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 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 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 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 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; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerEditor.java new file mode 100644 index 00000000..853605df --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerEditor.java @@ -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 + * **************************************************************************** + */ + +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 +{ + @Serial + private static final long serialVersionUID = 1L; + private static final Logger mLog = LoggerFactory.getLogger(BladeRfTunerEditor.class); + private JButton mTunerInfoButton; + private JComboBox mSampleRateCombo; + private JCheckBox mBiasTCheckBox; + private JComboBox 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 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 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 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 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("

bladeRF Tuner

"); + sb.append("Serial: ").append(info.serial()).append("
"); + sb.append("Board: ").append(info.board()).append("
"); + sb.append("Firmware: ").append(info.firmware()).append("
"); + sb.append("FPGA: ").append(info.fpga()).append("
"); + sb.append("USB Speed: ").append(info.speed()).append("
"); + return sb.toString(); + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java index e537d510..5d4f6843 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java +++ b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java @@ -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"), diff --git a/src/main/resources/88-nuand-bladerf.rules b/src/main/resources/88-nuand-bladerf.rules new file mode 100644 index 00000000..440e052e --- /dev/null +++ b/src/main/resources/88-nuand-bladerf.rules @@ -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" diff --git a/src/test/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerTest.java b/src/test/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerTest.java new file mode 100644 index 00000000..7086ddba --- /dev/null +++ b/src/test/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerTest.java @@ -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 + * **************************************************************************** + */ +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 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()); + } +}