diff --git a/build.gradle b/build.gradle
index ea4e9205..b3095ba0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -124,20 +124,16 @@ 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:jairspy:1.0.12' //Airspy tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jairspyhf:1.8.1' //Airspy HF+ tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jhackrf:2026.01.3' //HackRF tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jhydrasdr:1.1.4' //HydraSDR tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jlimesdr:25.1.0.1' //LimeSDR tuners - GitHub release, see settings.gradle
- implementation 'io.github.dsheirer:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jairspy:1.0.12.1' //Airspy tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jairspyhf:1.8.1.1' //Airspy HF+ tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jbladerf:2.6.0.1' //bladeRF tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jhackrf:2026.01.3.1' //HackRF tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jlibusb:1.0.30.1' //libusb and USB hotplug for the tuner libraries - GitHub release; not pulled in transitively
+ implementation 'io.github.dsheirer:jhydrasdr:1.1.4.1' //HydraSDR tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jlimesdr:25.1.0.2' //LimeSDR tuners - GitHub release, see settings.gradle
+ implementation 'io.github.dsheirer:jrtl:1.4.0.2' //RTL-SDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jsdrplay:3.15.3' //SDRplay RSP tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
- implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs
- implementation('io.github.dsheirer:usb4java:1.3.5') { //Fork with Loader support for OSX Multi-Version
- //Excludes the javax.usb API and the duplicate original usb4java classes. The native libraries are listed below.
- exclude group: 'org.usb4java', module: 'usb4java-javax'
- }
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.28.0'
implementation 'org.apache.commons:commons-lang3:3.20.0'
@@ -148,13 +144,6 @@ dependencies {
implementation 'org.controlsfx:controlsfx:11.2.3'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.17'
- implementation 'org.usb4java:libusb4java:1.3.0:darwin-x86-64' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:linux-aarch64' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:linux-arm' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:linux-x86' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:linux-x86-64' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:win32-x86' //usb4java native library
- implementation 'org.usb4java:libusb4java:1.3.0:win32-x86-64' //usb4java native library
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
diff --git a/settings.gradle b/settings.gradle
index 5286abe2..b0de5612 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -33,6 +33,15 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jrtl' }
}
+ //jlibusb is published as a GitHub release, with libusb for all platforms in the jar. Tuner libraries built on
+ //jlibusb (jAirspy, jAirspyHF, jBladeRF, jHackRF, jHydraSDR, jLimeSDR, jRTL) load their native library next to its libusb, so the process shares one copy.
+ ivy {
+ url = 'https://github.com/DSheirer/jlibusb/releases/download'
+ patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
+ metadataSources { artifact() }
+ content { includeModule 'io.github.dsheirer', 'jlibusb' }
+ }
+
//jHackRF is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jHackRF/releases/download'
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java
index 841b041e..0c2094a0 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java
@@ -32,19 +32,13 @@ 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.source.tuner.manager.UsbDeviceLookup;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* Airspy tuner controller, implemented with the jAirspy library (libairspy).
@@ -212,98 +206,17 @@ public class AirspyTunerController extends TunerController
/**
* Reads the serial number of the device at this controller's USB bus and port, since libairspy opens a specific
- * device by serial number. The device is closed again before returning so that libairspy can open it.
+ * device by serial number. The USB serial string is formatted as: AIRSPY SN:0123456789ABCDEF
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() 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));
- }
+ String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
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 readSerialNumber(device);
- }
- }
- }
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
- }
-
- throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
- }
-
- /**
- * Reads the serial number from the device's USB serial string, formatted as: AIRSPY SN:0123456789ABCDEF
- * @param device to read
- * @return serial number
- * @throws SourceException if the device can't be opened or the serial string isn't valid.
- */
- private static long readSerialNumber(Device device) throws SourceException
- {
- DeviceDescriptor descriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, descriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- DeviceHandle handle = new DeviceHandle();
- status = LibUsb.open(device, handle);
-
- if(status == LibUsb.ERROR_ACCESS)
- {
- mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
- throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
- }
- else if(status != LibUsb.SUCCESS)
- {
- mLog.error("Can't open USB tuner - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
- }
-
- String serial;
-
- try
- {
- serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
- }
- finally
- {
- LibUsb.close(handle);
- }
-
- try
- {
- return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1), 16);
+ return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1).trim(), 16);
}
catch(NullPointerException | NumberFormatException e)
{
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
index ada2b302..b1defcc1 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
@@ -32,18 +32,12 @@ 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.source.tuner.manager.UsbDeviceLookup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* Airspy HF+ Dual Port, HF+ Discovery and Ranger tuner controller, implemented with the jAirspyHF library
@@ -258,94 +252,13 @@ public class AirspyHfTunerController extends TunerController
/**
* Reads the serial number of the device at this controller's USB bus and port, since libairspyhf opens a specific
- * device by serial number. The device is closed again before returning so that libairspyhf can open it.
+ * device by serial number. The USB serial string is formatted as: AIRSPYHF SN:0123456789ABCDEF
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() 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 readSerialNumber(device);
- }
- }
- }
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
- }
-
- throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
- }
-
- /**
- * Reads the serial number from the device's USB serial string, formatted as: AIRSPYHF SN:0123456789ABCDEF
- * @param device to read
- * @return serial number
- * @throws SourceException if the device can't be opened or the serial string isn't valid.
- */
- private static long readSerialNumber(Device device) throws SourceException
- {
- DeviceDescriptor descriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, descriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- DeviceHandle handle = new DeviceHandle();
- status = LibUsb.open(device, handle);
-
- if(status == LibUsb.ERROR_ACCESS)
- {
- mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
- throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
- }
- else if(status != LibUsb.SUCCESS)
- {
- mLog.error("Can't open USB tuner - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
- }
-
- String serial;
-
- try
- {
- serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
- }
- finally
- {
- LibUsb.close(handle);
- }
+ String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
try
{
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
index 2200d279..5eeaafb8 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/bladerf/BladeRfTunerController.java
@@ -45,17 +45,13 @@ 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.source.tuner.manager.UsbDeviceLookup;
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).
@@ -376,46 +372,7 @@ public class BladeRfTunerController extends TunerController
*/
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 + "]");
+ return UsbDeviceLookup.find(mBus, mPortAddress).address();
}
@Override
diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDCommand.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDCommand.java
index fc345726..5e996984 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDCommand.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDCommand.java
@@ -20,7 +20,7 @@ package io.github.dsheirer.source.tuner.fcd;
public enum FCDCommand
{
/**
- * Note: the javaxUSB and usb4Java libraries are slightly different than
+ * Note: the libusb interrupt transfers used here (through jlibusb) are slightly different than
* the signal9 HID libraries used in C, so you will see variations in the
* byte array length and indexes, when comparing this setup to the qthid
* source code.
diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java
index 6d81432a..b39b9488 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java
@@ -19,6 +19,11 @@
package io.github.dsheirer.source.tuner.fcd;
import io.github.dsheirer.buffer.INativeBuffer;
+import io.github.dsheirer.libusb.LibUsb;
+import io.github.dsheirer.libusb.UsbContext;
+import io.github.dsheirer.libusb.UsbDeviceHandle;
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import io.github.dsheirer.libusb.UsbException;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.adapter.ComplexShortAdapter;
import io.github.dsheirer.source.SourceException;
@@ -28,21 +33,13 @@ import io.github.dsheirer.source.tuner.MixerTunerType;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.TunerType;
-import io.github.dsheirer.source.tuner.manager.TunerManager;
+import io.github.dsheirer.source.tuner.manager.UsbDeviceLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
-import org.usb4java.LibUsbException;
import javax.sound.sampled.TargetDataLine;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.nio.IntBuffer;
import java.util.concurrent.locks.ReentrantLock;
public abstract class FCDTunerController extends TunerController
@@ -50,16 +47,19 @@ public abstract class FCDTunerController extends TunerController
private final static Logger mLog = LoggerFactory.getLogger(FCDTunerController.class);
private static final double USABLE_BANDWIDTH_PERCENT = 1.0;
private static final int DC_SPIKE_AVOID_HALF_BANDWIDTH = 5000;
- private final static byte FCD_INTERFACE = (byte) 0x2;
- private final static byte FCD_ENDPOINT_IN = (byte) 0x82;
- private final static byte FCD_ENDPOINT_OUT = (byte) 0x2;
+ private final static int FCD_INTERFACE = 0x2;
+ private final static int FCD_ENDPOINT_IN = 0x82;
+ private final static int FCD_ENDPOINT_OUT = 0x2;
+ private final static int FCD_PACKET_SIZE = 64;
+ private final static int FCD_TRANSFER_TIMEOUT_MS = 500;
+ private final static int LIBUSB_ERROR_NO_DEVICE = -4;
private int mBus;
private String mPortAddress;
- private Context mDeviceContext = new Context();
- private Device mDevice;
- private DeviceDescriptor mDeviceDescriptor = new DeviceDescriptor();
- private DeviceHandle mDeviceHandle = new DeviceHandle();
+ private UsbContext mUsbContext;
+ private UsbDeviceInfo mDevice;
+ private volatile UsbDeviceHandle mDeviceHandle;
+ private boolean mShutdown = false;
private FCDConfiguration mConfiguration = new FCDConfiguration();
protected ComplexMixer mComplexMixer;
@@ -168,58 +168,6 @@ public abstract class FCDTunerController extends TunerController
}
}
- /**
- * Finds the USB device for this tuner at the specified USB bus and port.
- * @return discovered USB device
- * @throws SourceException if there is an error or the device is not discovered.
- */
- private Device findDevice() throws SourceException
- {
- Device foundDevice = null;
-
- DeviceList deviceList = new DeviceList();
- int count = LibUsb.getDeviceList(mDeviceContext, deviceList);
-
- if(count >= 0)
- {
- for(Device device: deviceList)
- {
- int bus = LibUsb.getBusNumber(device);
- int port = LibUsb.getPortNumber(device);
-
- if(port > 0)
- {
- String portAddress = TunerManager.getPortAddress(device);
-
- if(mBus == bus && mPortAddress != null && mPortAddress.equals(portAddress))
- {
- foundDevice = device;
- }
- else
- {
- LibUsb.unrefDevice(device);
- }
- }
- else
- {
- LibUsb.unrefDevice(device);
- }
- }
- }
- else
- {
- throw new SourceException("LibUsb couldn't discover USB device [" + mBus + ":" + mPortAddress +
- "] from device list" + (count < 0 ? " - error: " + LibUsb.errorName(count) : ""));
- }
-
- if(foundDevice == null)
- {
- throw new SourceException("LibUsb couldn't find the matching USB device");
- }
-
- return foundDevice;
- }
-
/**
* Initializes the controller by opening the USB device and claiming the HID interface.
* Invoke this method after constructing this class to setup the controller.
@@ -228,80 +176,102 @@ public abstract class FCDTunerController extends TunerController
*/
public void start() throws SourceException
{
- if(mDeviceContext == null)
+ if(mShutdown)
{
throw new SourceException("Device cannot be reused once it has been shutdown");
}
- int status = LibUsb.init(mDeviceContext);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
- }
-
- mDevice = findDevice();
- mDeviceDescriptor = new DeviceDescriptor();
- status = LibUsb.getDeviceDescriptor(mDevice, mDeviceDescriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- mDeviceDescriptor = null;
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- mDeviceHandle = new DeviceHandle();
- status = LibUsb.open(mDevice, mDeviceHandle);
-
- if(status != LibUsb.SUCCESS)
- {
- mDeviceHandle = null;
- mDeviceDescriptor = null;
-
- mLog.error("Can't open USB tuner [" + getTunerType() + "] - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - check driver or Linux udev rules - " + LibUsb.errorName(status));
- }
-
- //Remove a kernel driver if active
- status = LibUsb.kernelDriverActive(mDeviceHandle, FCD_INTERFACE);
-
- if(status == 1)
- {
- status = LibUsb.detachKernelDriver(mDeviceHandle, FCD_INTERFACE);
-
- if(status != LibUsb.SUCCESS)
- {
- mLog.error("Unable to detach kernel driver for USB tuner device - bus:" + mBus + " port:" + mPortAddress);
- mDeviceHandle = null;
- mDeviceDescriptor = null;
- throw new SourceException("Can't detach kernel driver");
- }
- }
-
- //Claim the interface
- status = LibUsb.claimInterface(mDeviceHandle, FCD_INTERFACE);
-
- if(status == LibUsb.ERROR_BUSY)
- {
- mDeviceHandle = null;
- mDeviceDescriptor = null;
- throw new SourceException("USB tuner is in-use by another application");
- }
- else if(status != LibUsb.SUCCESS)
- {
- mDeviceHandle = null;
- mDeviceDescriptor = null;
- throw new SourceException("Can't claim interface on USB tuner - " + LibUsb.errorName(status));
- }
+ mUsbContext = UsbDeviceLookup.createContext();
try
{
- deviceStart();
+ mDevice = UsbDeviceLookup.find(mUsbContext, mBus, mPortAddress);
+
+ try
+ {
+ mDeviceHandle = mUsbContext.open(mDevice);
+ }
+ catch(UsbException e)
+ {
+ mLog.error("Can't open USB tuner [" + getTunerType() + "] - check driver or Linux udev rules");
+ throw new SourceException("Can't open USB tuner - check driver or Linux udev rules - " +
+ LibUsb.errorName(e.code()));
+ }
+
+ //Remove a kernel driver if active
+ boolean kernelDriverActive;
+
+ try
+ {
+ kernelDriverActive = mDeviceHandle.isKernelDriverActive(FCD_INTERFACE);
+ }
+ catch(UsbException e)
+ {
+ kernelDriverActive = false;
+ }
+
+ if(kernelDriverActive)
+ {
+ try
+ {
+ mDeviceHandle.detachKernelDriver(FCD_INTERFACE);
+ }
+ catch(UsbException e)
+ {
+ mLog.error("Unable to detach kernel driver for USB tuner device - bus:" + mBus + " port:" + mPortAddress);
+ throw new SourceException("Can't detach kernel driver");
+ }
+ }
+
+ //Claim the interface
+ try
+ {
+ mDeviceHandle.claimInterface(FCD_INTERFACE);
+ }
+ catch(UsbException e)
+ {
+ if(e.code() == UsbDeviceLookup.ERROR_BUSY)
+ {
+ throw new SourceException("USB tuner is in-use by another application");
+ }
+
+ throw new SourceException("Can't claim interface on USB tuner - " + LibUsb.errorName(e.code()));
+ }
}
catch(SourceException se)
{
+ closeUsb();
throw se;
}
+
+ deviceStart();
+ }
+
+ /**
+ * Closes the USB device handle and the libusb context.
+ */
+ private void closeUsb()
+ {
+ UsbDeviceHandle handle = mDeviceHandle;
+ mDeviceHandle = null;
+
+ if(handle != null)
+ {
+ try
+ {
+ handle.close();
+ }
+ catch(Exception e)
+ {
+ mLog.error("error while closing device handle", e);
+ }
+ }
+
+ if(mUsbContext != null)
+ {
+ mUsbContext.close();
+ mUsbContext = null;
+ }
}
/**
@@ -321,23 +291,9 @@ public abstract class FCDTunerController extends TunerController
mComplexMixer = null;
}
- if(mDeviceHandle != null)
- {
- try
- {
- LibUsb.close(mDeviceHandle);
- }
- catch(Exception e)
- {
- mLog.error("error while closing device handle", e);
- }
- }
-
- mDeviceDescriptor = null;
- mDeviceHandle = null;
+ closeUsb();
mDevice = null;
- LibUsb.exit(mDeviceContext);
- mDeviceContext = null;
+ mShutdown = true;
}
/**
@@ -360,19 +316,14 @@ public abstract class FCDTunerController extends TunerController
*/
public String getUSBAddress()
{
- if(mDevice != null)
+ UsbDeviceInfo device = mDevice;
+
+ //This is the tuner's unique ID for saved tuner configurations, so the format must not change: the port is the
+ //last port number in the path from the root hub, as libusb_get_port_number() reports it.
+ if(device != null)
{
- StringBuilder sb = new StringBuilder();
-
- sb.append("Bus:");
- int bus = LibUsb.getBusNumber(mDevice);
- sb.append(bus);
-
- sb.append(" Port:");
- int port = LibUsb.getPortNumber(mDevice);
- sb.append(port);
-
- return sb.toString();
+ int port = device.portNumbers().isEmpty() ? 0 : device.portNumbers().getLast();
+ return "Bus:" + device.busNumber() + " Port:" + port;
}
return "UNKNOWN";
@@ -383,38 +334,34 @@ public abstract class FCDTunerController extends TunerController
*/
public String getUSBID()
{
- if(mDeviceDescriptor != null)
+ UsbDeviceInfo device = mDevice;
+
+ if(device != null)
{
- StringBuilder sb = new StringBuilder();
-
- sb.append(String.format("%04X", (int) (mDeviceDescriptor.idVendor() & 0xFFFF)));
- sb.append(":");
- sb.append(String.format("%04X", (int) (mDeviceDescriptor.idProduct() & 0xFFFF)));
-
- return sb.toString();
+ return String.format("%04X:%04X", device.vendorId(), device.productId());
}
return "UNKNOWN";
}
/**
- * USB Port Speed. Should be 2.0 for both types of funcube dongles
+ * USB Port Speed. The FUNcube Dongle Pro is a full speed (USB 1.1) device.
*/
public String getUSBSpeed()
{
- if(mDevice != null)
- {
- int speed = LibUsb.getDeviceSpeed(mDevice);
+ UsbDeviceInfo device = mDevice;
- switch(speed)
+ if(device != null)
+ {
+ switch(device.speed())
{
- case 0:
+ case LOW:
return "1.1 LOW";
- case 1:
+ case FULL:
return "1.1 FULL";
- case 2:
+ case HIGH:
return "2.0 HIGH";
- case 3:
+ case SUPER:
return "3.0 SUPER";
default:
}
@@ -500,26 +447,27 @@ public abstract class FCDTunerController extends TunerController
}
/**
- * Performs an interrupt write to the OUT endpoint.
+ * Performs an interrupt write of one 64 byte packet to the OUT endpoint.
*
- * @param buffer - direct allocated buffer. Must be 64 bytes in length.
- * @throws LibUsbException on error
+ * @param packet to write. Must be 64 bytes in length.
+ * @throws FCDUsbException on error
*/
- private void write(ByteBuffer buffer) throws LibUsbException
+ private void write(byte[] packet) throws FCDUsbException
{
- if(mDeviceHandle != null)
- {
- IntBuffer transferred = IntBuffer.allocate(1);
- int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_OUT, buffer, transferred, 500l);
+ UsbDeviceHandle handle = mDeviceHandle;
- if(result != LibUsb.SUCCESS)
- {
- throw new LibUsbException("error writing byte buffer", result);
- }
- }
- else
+ if(handle == null)
{
- throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE);
+ throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
+ }
+
+ try
+ {
+ handle.interruptTransfer(FCD_ENDPOINT_OUT, packet, packet.length, FCD_TRANSFER_TIMEOUT_MS);
+ }
+ catch(UsbException e)
+ {
+ throw new FCDUsbException("error writing byte buffer", e.code());
}
}
@@ -527,14 +475,14 @@ public abstract class FCDTunerController extends TunerController
* Performs an interrupt write to the OUT endpoint for the FCD command.
*
* @param command - no-argument command to write
- * @throws LibUsbException - on error
+ * @throws FCDUsbException - on error
*/
- private void write(FCDCommand command) throws LibUsbException
+ private void write(FCDCommand command) throws FCDUsbException
{
- ByteBuffer buffer = ByteBuffer.allocateDirect(64);
- buffer.put(0, command.getCommand());
- buffer.put(1, (byte) 0x00);
- write(buffer);
+ byte[] packet = new byte[FCD_PACKET_SIZE];
+ packet[0] = command.getCommand();
+ packet[1] = (byte) 0x00;
+ write(packet);
}
/**
@@ -542,46 +490,53 @@ public abstract class FCDTunerController extends TunerController
*
* @param command - command to write
* @param argument - value to write with the command
- * @throws LibUsbException - on error
+ * @throws FCDUsbException - on error
*/
- private void write(FCDCommand command, long argument) throws LibUsbException
+ private void write(FCDCommand command, long argument) throws FCDUsbException
{
- ByteBuffer buffer = ByteBuffer.allocateDirect(64);
+ byte[] packet = new byte[FCD_PACKET_SIZE];
+ ByteBuffer buffer = ByteBuffer.wrap(packet);
/* The FCD expects little-endian formatted values */
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(0, command.getCommand());
buffer.putLong(1, argument);
- write(buffer);
+ write(packet);
}
/**
- * Performs an interrupt read against the endpoint
+ * Performs an interrupt read of one 64 byte packet from the IN endpoint
*
* @return buffer read from FCD
- * @throws LibUsbException on error
+ * @throws FCDUsbException on error
*/
- private ByteBuffer read() throws LibUsbException
+ private ByteBuffer read() throws FCDUsbException
{
- if(mDeviceHandle != null)
+ UsbDeviceHandle handle = mDeviceHandle;
+
+ if(handle == null)
{
- ByteBuffer buffer = ByteBuffer.allocateDirect(64);
- IntBuffer transferred = IntBuffer.allocate(1);
- int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_IN, buffer, transferred, 500l);
-
- if(result != LibUsb.SUCCESS)
- {
- throw new LibUsbException("error reading byte buffer", result);
- }
- else if(transferred.get(0) != buffer.capacity())
- {
- throw new LibUsbException("received bytes [" + transferred.get(0) + "] didn't match expected length [" +
- buffer.capacity() + "]", result);
- }
-
- return buffer;
+ throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
}
- throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE);
+ byte[] packet = new byte[FCD_PACKET_SIZE];
+ int transferred;
+
+ try
+ {
+ transferred = handle.interruptTransfer(FCD_ENDPOINT_IN, packet, packet.length, FCD_TRANSFER_TIMEOUT_MS);
+ }
+ catch(UsbException e)
+ {
+ throw new FCDUsbException("error reading byte buffer", e.code());
+ }
+
+ if(transferred != packet.length)
+ {
+ throw new FCDUsbException("received bytes [" + transferred + "] didn't match expected length [" +
+ packet.length + "]", 0);
+ }
+
+ return ByteBuffer.wrap(packet);
}
/**
@@ -590,9 +545,9 @@ public abstract class FCDTunerController extends TunerController
*
* @param command - command to send
* @param argument - command argument to send
- * @throws LibUsbException - on error
+ * @throws FCDUsbException - on error
*/
- protected synchronized void send(FCDCommand command, long argument) throws LibUsbException
+ protected synchronized void send(FCDCommand command, long argument) throws FCDUsbException
{
write(command, argument);
read();
@@ -603,9 +558,9 @@ public abstract class FCDTunerController extends TunerController
* command.
*
* @param command - command to send
- * @throws LibUsbException - on error
+ * @throws FCDUsbException - on error
*/
- protected ByteBuffer send(FCDCommand command) throws LibUsbException
+ protected ByteBuffer send(FCDCommand command) throws FCDUsbException
{
write(command);
return read();
diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDUsbException.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDUsbException.java
new file mode 100644
index 00000000..0a0ad82a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDUsbException.java
@@ -0,0 +1,50 @@
+/*
+ * *****************************************************************************
+ * 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.fcd;
+
+import io.github.dsheirer.libusb.LibUsb;
+
+/**
+ * USB transfer error while communicating with a FUNcube Dongle. Unchecked, like the usb4java exception it replaces,
+ * since the FCD command methods are called from many places that handle any exception.
+ */
+public class FCDUsbException extends RuntimeException
+{
+ private final int mErrorCode;
+
+ /**
+ * Constructs an instance
+ * @param message describing the failed operation
+ * @param errorCode libusb error code, or 0 when the transfer succeeded but returned unexpected data
+ */
+ public FCDUsbException(String message, int errorCode)
+ {
+ super(errorCode < 0 ? message + " - " + LibUsb.errorName(errorCode) : message);
+ mErrorCode = errorCode;
+ }
+
+ /**
+ * libusb error code, such as -7 for LIBUSB_ERROR_TIMEOUT, or 0
+ */
+ public int getErrorCode()
+ {
+ return mErrorCode;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java
index b62d277a..71bab1dd 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java
@@ -32,17 +32,11 @@ 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.source.tuner.manager.UsbDeviceLookup;
import java.util.EnumSet;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* HackRF One, HackRF Pro, Jawbreaker and rad1o tuner controller, implemented with the jHackRF library (libhackrf).
@@ -252,110 +246,21 @@ public class HackRFTunerController extends TunerController
/**
* Reads the USB serial number of the device at this controller's USB bus and port, since libhackrf opens a
- * specific device by serial number. The device is closed again before returning so that libhackrf can open it.
+ * specific device by serial number.
* @return serial number or null if the device doesn't report a serial number.
* @throws SourceException if the device isn't found or can't be accessed.
*/
private String readSerialNumber() throws SourceException
{
- Context context = new Context();
- int status = LibUsb.init(context);
+ String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
- if(status != LibUsb.SUCCESS)
+ //libhackrf compares, at most, the last 32 characters of the serial string
+ if(serial != null && serial.length() > 32)
{
- throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
+ serial = serial.substring(serial.length() - 32);
}
- 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 readSerialNumber(device);
- }
- }
- }
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
- }
-
- throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
- }
-
- /**
- * Reads the serial number from the device's USB serial string descriptor.
- * @param device to read
- * @return serial number or null if the device doesn't have a serial string.
- * @throws SourceException if the device can't be opened.
- */
- private static String readSerialNumber(Device device) throws SourceException
- {
- DeviceDescriptor descriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, descriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- if(descriptor.iSerialNumber() == 0)
- {
- return null;
- }
-
- DeviceHandle handle = new DeviceHandle();
- status = LibUsb.open(device, handle);
-
- if(status == LibUsb.ERROR_ACCESS)
- {
- mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
- throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
- }
- else if(status != LibUsb.SUCCESS)
- {
- mLog.error("Can't open USB tuner - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
- }
-
- try
- {
- String serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
-
- if(serial != null)
- {
- serial = serial.trim();
-
- //libhackrf compares, at most, the last 32 characters of the serial string
- if(serial.length() > 32)
- {
- serial = serial.substring(serial.length() - 32);
- }
- }
-
- return serial == null || serial.isEmpty() ? null : serial;
- }
- finally
- {
- LibUsb.close(handle);
- }
+ return serial == null || serial.isEmpty() ? null : serial;
}
/**
diff --git a/src/main/java/io/github/dsheirer/source/tuner/hydrasdr/HydraSdrTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hydrasdr/HydraSdrTunerController.java
index f359de8e..f82ebc2f 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/hydrasdr/HydraSdrTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/hydrasdr/HydraSdrTunerController.java
@@ -34,19 +34,13 @@ 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.source.tuner.manager.UsbDeviceLookup;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* HydraSDR tuner controller, implemented with the jHydraSDR library (libhydrasdr).
@@ -209,98 +203,17 @@ public class HydraSdrTunerController extends TunerController
/**
* Reads the serial number of the device at this controller's USB bus and port, since libhydrasdr opens a specific
- * device by serial number. The device is closed again before returning so that libhydrasdr can open it.
+ * device by serial number. The USB serial string is formatted as: HYDRASDR SN:0123456789ABCDEF
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() 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));
- }
+ String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
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 readSerialNumber(device);
- }
- }
- }
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
- }
-
- throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
- }
-
- /**
- * Reads the serial number from the device's USB serial string, formatted as: HYDRASDR SN:0123456789ABCDEF
- * @param device to read
- * @return serial number
- * @throws SourceException if the device can't be opened or the serial string isn't valid.
- */
- private static long readSerialNumber(Device device) throws SourceException
- {
- DeviceDescriptor descriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, descriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- DeviceHandle handle = new DeviceHandle();
- status = LibUsb.open(device, handle);
-
- if(status == LibUsb.ERROR_ACCESS)
- {
- mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
- throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
- }
- else if(status != LibUsb.SUCCESS)
- {
- mLog.error("Can't open USB tuner - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
- }
-
- String serial;
-
- try
- {
- serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
- }
- finally
- {
- LibUsb.close(handle);
- }
-
- try
- {
- return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1), 16);
+ return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1).trim(), 16);
}
catch(NullPointerException | NumberFormatException e)
{
diff --git a/src/main/java/io/github/dsheirer/source/tuner/limesdr/LimeSdrTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/limesdr/LimeSdrTunerController.java
index 84e58f73..b3136ac6 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/limesdr/LimeSdrTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/limesdr/LimeSdrTunerController.java
@@ -19,6 +19,11 @@
package io.github.dsheirer.source.tuner.limesdr;
+import io.github.dsheirer.libusb.LibUsb;
+import io.github.dsheirer.libusb.UsbContext;
+import io.github.dsheirer.libusb.UsbDeviceHandle;
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import io.github.dsheirer.libusb.UsbException;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.limesdr.DeviceEntry;
@@ -38,7 +43,7 @@ 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.source.tuner.manager.UsbDeviceLookup;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.List;
@@ -49,12 +54,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* LimeSDR tuner controller, implemented with the jLimeSDR library (LimeSuiteNG).
@@ -448,103 +447,42 @@ public class LimeSdrTunerController extends TunerController
*/
private UsbIdentity readUsbIdentity() throws SourceException
{
- Context context = new Context();
- int status = LibUsb.init(context);
-
- if(status != LibUsb.SUCCESS)
+ try(UsbContext usb = UsbDeviceLookup.createContext())
{
- throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
- }
+ UsbDeviceInfo device = UsbDeviceLookup.find(usb, mBus, mPortAddress);
+ String serial = null;
- try
- {
- DeviceList deviceList = new DeviceList();
- int count = LibUsb.getDeviceList(context, deviceList);
-
- if(count < 0)
+ if(device.serialNumberIndex() != 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 readUsbIdentity(device);
- }
- }
- }
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
- }
-
- throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
- }
-
- /**
- * Reads the vendor ID, product ID and serial number of the USB device.
- */
- private static UsbIdentity readUsbIdentity(Device device) throws SourceException
- {
- DeviceDescriptor descriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, descriptor);
-
- if(status != LibUsb.SUCCESS)
- {
- throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
- }
-
- int vendor = descriptor.idVendor() & 0xFFFF;
- int product = descriptor.idProduct() & 0xFFFF;
- String serial = null;
-
- if(descriptor.iSerialNumber() != 0)
- {
- DeviceHandle handle = new DeviceHandle();
- status = LibUsb.open(device, handle);
-
- if(status == LibUsb.SUCCESS)
- {
- try
+ try(UsbDeviceHandle handle = usb.open(device))
{
//LimeSuiteNG aborts the application when it opens a device that another application is using,
//since the open throws a C++ exception through its C API. Check that the device is free first.
- status = LibUsb.claimInterface(handle, 0);
-
- if(status == LibUsb.ERROR_BUSY)
+ try
{
- throw new SourceException("LimeSDR is in use by another application");
+ handle.claimInterface(0);
+ handle.releaseInterface(0);
}
- else if(status == LibUsb.SUCCESS)
+ catch(UsbException e)
{
- LibUsb.releaseInterface(handle, 0);
+ if(e.code() == UsbDeviceLookup.ERROR_BUSY)
+ {
+ throw new SourceException("LimeSDR is in use by another application");
+ }
}
- serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
- serial = serial != null && !serial.isBlank() ? serial.trim() : null;
+ serial = handle.readString(device.serialNumberIndex());
+ serial = serial != null && !serial.isBlank() ? serial : null;
}
- finally
+ catch(UsbException e)
{
- LibUsb.close(handle);
+ //Expected on Windows, where libusb can't open a device that uses the Cypress or FTDI driver
+ mLog.debug("Can't open LimeSDR to read its USB serial number - " + LibUsb.errorName(e.code()));
}
}
- else
- {
- //Expected on Windows, where libusb can't open a device that uses the Cypress or FTDI driver
- mLog.debug("Can't open LimeSDR to read its USB serial number - " + LibUsb.errorName(status));
- }
- }
- return new UsbIdentity(vendor, product, serial);
+ return new UsbIdentity(device.vendorId(), device.productId(), serial);
+ }
}
@Override
diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
index 5690984b..1c6e6a0a 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
@@ -20,6 +20,13 @@
package io.github.dsheirer.source.tuner.manager;
import io.github.dsheirer.gui.preference.tuner.RspDuoSelectionMode;
+import io.github.dsheirer.libusb.LibUsb;
+import io.github.dsheirer.libusb.UsbContext;
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import io.github.dsheirer.libusb.UsbException;
+import io.github.dsheirer.libusb.UsbHotplug;
+import io.github.dsheirer.libusb.UsbHotplugListener;
+import io.github.dsheirer.libusb.UsbLogLevel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.source.ChannelizerType;
import io.github.dsheirer.sdrplay.DeviceInfo;
@@ -45,22 +52,13 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.DiscoveredRspDuoTuner1;
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
import io.github.dsheirer.util.ThreadPool;
-import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
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.DeviceDescriptor;
-import org.usb4java.DeviceList;
-import org.usb4java.HotplugCallback;
-import org.usb4java.HotplugCallbackHandle;
-import org.usb4java.LibUsb;
/**
* Tuner manager provides access to tuners using USB, recording, sound-card and system-daemon accessible devices. This
@@ -73,9 +71,12 @@ public class TunerManager implements IDiscoveredTunerStatusListener
private final UserPreferences mUserPreferences;
private final DiscoveredTunerModel mDiscoveredTunerModel;
private final TunerConfigurationManager mTunerConfigurationManager;
+ /**
+ * libusb error code for an unspecified error (LIBUSB_ERROR_OTHER)
+ */
+ private static final int LIBUSB_ERROR_OTHER = -99;
private final HotplugEventSupport mHotplugEventSupport = new HotplugEventSupport();
- private final Context mLibUsbApplicationContext = new Context();
- private boolean mLibUsbInitialized = false;
+ private boolean mLibUsbAvailable = false;
private SDRplay mSDRplay;
/**
@@ -112,60 +113,26 @@ public class TunerManager implements IDiscoveredTunerStatusListener
{
mLog.info("Discovering tuners ...");
- boolean libUsbAvailable = false;
-
try
{
- int api = LibUsb.getApiVersion();
- libUsbAvailable = true;
- mLog.info("LibUsb API Version: " + ((api >> 24) & 0xFF) + "." + ((api >> 16) & 0xFF) + "." + (api & 0xFFFF));
- mLog.info("LibUsb Version: " + LibUsb.getVersion());
+ //Loads the libusb library that jlibusb bundles, which the USB tuner libraries share
+ mLog.info("LibUsb Version: " + LibUsb.version() + " - hotplug detection: " +
+ (LibUsb.hasHotplug() ? "native" : "polling"));
+ mLibUsbAvailable = true;
}
- catch(Exception e)
+ catch(LinkageError | RuntimeException e)
{
- final String os = System.getProperty("os.name").toLowerCase();
- final String version = System.getProperty("os.version");
-
- //Special handling for MacOS Tahoe version 26 issue with LibUsb.
- if(os != null && os.startsWith("macos") && version != null && version.startsWith("26"))
- {
- mLog.warn("Unable to load libusb library for communication with USB tuner(s)");
- mLog.warn("Mac OS Tahoe [" + os + "] version [" + version + "] detected. There is an issue");
- mLog.warn("with libusb for this OS version. You might consider manually installing the latest");
- mLog.warn("version of the libusb library. In a terminal window type:");
- mLog.warn("");
- mLog.warn("\t\tbrew install libusb --HEAD");
- mLog.warn("");
- }
- else
- {
- mLog.error("Error while loading libusb for os [" + os + "] version [" + version + "]", e);
- }
+ String os = System.getProperty("os.name");
+ String version = System.getProperty("os.version");
+ mLog.error("Unable to load the libusb library for communication with USB tuner(s) on os [" + os +
+ "] version [" + version + "]. To use a different libusb build, start sdrtrunk with " +
+ "-Djlibusb.library.path=", e);
}
- if(libUsbAvailable)
+ if(mLibUsbAvailable)
{
- try
- {
- int status = LibUsb.init(mLibUsbApplicationContext);
-
- if(status == LibUsb.SUCCESS)
- {
- mLibUsbInitialized = true;
- // LibUsb.setOption(mLibUsbApplicationContext, LibUsb.OPTION_LOG_LEVEL, LibUsb.LOG_LEVEL_DEBUG);
- discoverUSBTuners();
- }
- }
- catch(Exception e)
- {
- mLog.error("Error initializing LibUsb and usb4java library", e);
- }
-
- if(mLibUsbInitialized)
- {
- mHotplugEventSupport.start();
- }
-
+ discoverUSBTuners();
+ mHotplugEventSupport.start();
discoverSdrPlayTuners();
}
@@ -177,19 +144,14 @@ public class TunerManager implements IDiscoveredTunerStatusListener
*/
public void stop()
{
+ //Stop hotplug detection first so that no tuner is added or removed while the tuners are released
+ mHotplugEventSupport.stop();
+
//Stop all tuners
mDiscoveredTunerModel.releaseDiscoveredTuners();
//Shutdown SDRplay API instance
closeSDRplay();
-
- //Shutdown LibUsb
- if(mLibUsbInitialized)
- {
- mHotplugEventSupport.stop();
- LibUsb.exit(mLibUsbApplicationContext);
- mLibUsbInitialized = false;
- }
}
/**
@@ -205,101 +167,123 @@ public class TunerManager implements IDiscoveredTunerStatusListener
*/
private void discoverUSBTuners()
{
- if(mLibUsbInitialized)
+ List discoveredUSBTuners = new ArrayList<>();
+
+ try(UsbContext usb = UsbContext.create())
{
- List discoveredUSBTuners = new ArrayList<>();
+ List devices = getDeviceList(usb);
- try
+ if(devices != null)
{
- DeviceList deviceList = new DeviceList();
- int deviceCount = LibUsb.getDeviceList(mLibUsbApplicationContext, deviceList);
+ mLog.info("LibUsb - discovered [" + devices.size() + "] potential usb devices");
- if(deviceCount >= 0)
+ for(UsbDeviceInfo device: devices)
{
- mLog.info("LibUsb - discovered [" + deviceCount + "] potential usb devices");
+ DiscoveredUSBTuner discoveredUSBTuner = createDiscoveredTuner(device);
- for(Device device: deviceList)
+ if(discoveredUSBTuner != null)
{
- int bus = LibUsb.getBusNumber(device);
- int port = LibUsb.getPortNumber(device);
-
- if(port > 0)
- {
- DeviceDescriptor deviceDescriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, deviceDescriptor);
-
- if(status == LibUsb.SUCCESS)
- {
- TunerClass tunerClass = TunerClass.lookup(deviceDescriptor.idVendor(),
- deviceDescriptor.idProduct());
-
- String portAddress = getPortAddress(device);
-
- if(tunerClass.isSupportedUsbTuner())
- {
- mLog.info("Discovered tuner at USB Bus [" + bus + "] Port [" + portAddress +
- "] Tuner Class [" + tunerClass + "]");
- ChannelizerType channelizerType = mUserPreferences.getTunerPreference().getChannelizerType();
- DiscoveredUSBTuner discoveredUSBTuner = new DiscoveredUSBTuner(tunerClass, bus,
- portAddress, channelizerType);
- discoveredUSBTuners.add(discoveredUSBTuner);
- }
- }
- else
- {
- mLog.error("LibUsb - unable to get device descriptor for device on bus [" + bus +
- "] port [" + port + "] - status [" + status + "] - " + LibUsb.errorName(status));
- }
- }
-
- //Unref the device - it will be rediscovered under the device context when it is started
- LibUsb.unrefDevice(device);
- }
-
- LibUsb.freeDeviceList(deviceList, false);
- }
- //If the get device list operation generated error -99, turn on warning logging and try device list
- // again to see what's causing it.
- else if(deviceCount == LibUsb.ERROR_OTHER)
- {
- try
- {
- mLog.info("LibUsb reported error -99 during get device list - increasing logging level and retrying");
- LibUsb.setOption(mLibUsbApplicationContext, LibUsb.OPTION_LOG_LEVEL, LibUsb.LOG_LEVEL_WARNING);
- deviceCount = LibUsb.getDeviceList(mLibUsbApplicationContext, deviceList);
- if(deviceCount >=0)
- {
- mLog.info("LibUsb - get device list count [" + deviceCount + "] during retry");
- LibUsb.freeDeviceList(deviceList, false);
- }
- else
- {
- mLog.info("LibUsb - get device list reported error [" + deviceCount + "/" +
- LibUsb.errorName(deviceCount) + "] during retry");
- }
- LibUsb.setOption(mLibUsbApplicationContext, LibUsb.OPTION_LOG_LEVEL, LibUsb.LOG_LEVEL_NONE);
- mLog.info("LibUse - get device list reattempt complete - if additional logging was generated please notify developer");
- }
- catch(Exception e)
- {
- mLog.info("LibUsb error during get device list reattempt", e);
+ mLog.info("Discovered tuner at USB Bus [" + discoveredUSBTuner.getBus() + "] Port [" +
+ discoveredUSBTuner.getPortAddress() + "] Tuner Class [" +
+ discoveredUSBTuner.getTunerClass() + "]");
+ discoveredUSBTuners.add(discoveredUSBTuner);
}
}
- else
- {
- mLog.error("LibUsb - error [" + deviceCount + "/" + LibUsb.errorName(deviceCount) + "] during get device list");
- }
- }
- catch(Exception e)
- {
- mLog.error("LibUsb - error during USB device discovery", e);
- }
-
- for(DiscoveredUSBTuner discoveredUSBTuner: discoveredUSBTuners)
- {
- addUsbTuner(discoveredUSBTuner);
}
}
+ catch(UsbException | RuntimeException e)
+ {
+ mLog.error("LibUsb - error during USB device discovery", e);
+ }
+
+ for(DiscoveredUSBTuner discoveredUSBTuner: discoveredUSBTuners)
+ {
+ addUsbTuner(discoveredUSBTuner);
+ }
+ }
+
+ /**
+ * Lists the USB devices. If listing fails with error -99, retries once with libusb warning logging turned on so
+ * that the log shows what caused it.
+ * @param usb context to list devices from
+ * @return devices, or null if they couldn't be listed
+ */
+ private List getDeviceList(UsbContext usb)
+ {
+ try
+ {
+ return usb.devices();
+ }
+ catch(UsbException e)
+ {
+ if(e.code() != LIBUSB_ERROR_OTHER)
+ {
+ mLog.error("LibUsb - error during get device list - " + e.getMessage());
+ return null;
+ }
+ }
+
+ try
+ {
+ mLog.info("LibUsb reported error -99 during get device list - increasing logging level and retrying");
+ usb.setLogLevel(UsbLogLevel.WARNING);
+ List devices = usb.devices();
+ mLog.info("LibUsb - get device list count [" + devices.size() + "] during retry");
+ return devices;
+ }
+ catch(UsbException e)
+ {
+ mLog.info("LibUsb - get device list reported error during retry - " + e.getMessage());
+ return null;
+ }
+ finally
+ {
+ try
+ {
+ usb.setLogLevel(UsbLogLevel.NONE);
+ }
+ catch(UsbException e)
+ {
+ //Logging level only - nothing else to do
+ }
+
+ mLog.info("LibUsb - get device list reattempt complete - if additional logging was generated please notify developer");
+ }
+ }
+
+ /**
+ * Identifies the USB tuner class of a device.
+ * @param device to identify
+ * @return tuner class, or null for a root hub or a device that isn't a supported tuner
+ */
+ private static TunerClass getSupportedTunerClass(UsbDeviceInfo device)
+ {
+ //A root hub has no port numbers
+ if(device.portNumbers().isEmpty())
+ {
+ return null;
+ }
+
+ TunerClass tunerClass = TunerClass.lookup((short)device.vendorId(), (short)device.productId());
+ return tunerClass.isSupportedUsbTuner() ? tunerClass : null;
+ }
+
+ /**
+ * Creates a discovered tuner for a USB device.
+ * @param device to create a tuner for
+ * @return discovered tuner, or null if the device isn't a supported tuner
+ */
+ private DiscoveredUSBTuner createDiscoveredTuner(UsbDeviceInfo device)
+ {
+ TunerClass tunerClass = getSupportedTunerClass(device);
+
+ if(tunerClass == null)
+ {
+ return null;
+ }
+
+ ChannelizerType channelizerType = mUserPreferences.getTunerPreference().getChannelizerType();
+ return new DiscoveredUSBTuner(tunerClass, device.busNumber(), UsbPortAddress.of(device), channelizerType);
}
/**
@@ -680,161 +664,104 @@ public class TunerManager implements IDiscoveredTunerStatusListener
}
/**
- * Creates a dotted-notation string representing the port numbers for the device from the root hub through any
- * intermediate hubs.
- * @param device to inspect
- * @return formatted address
- */
- public static String getPortAddress(Device device)
- {
- ByteBuffer portNumberArray = ByteBuffer.allocateDirect(7); //7 ports maximum.
- int portCount = LibUsb.getPortNumbers(device, portNumberArray);
- portNumberArray.rewind();
-
- StringBuilder sb = new StringBuilder();
-
- for(int x = 0; x < portCount; x++)
- {
- if(x > 0)
- {
- sb.append(".");
- }
-
- sb.append(portNumberArray.get());
- }
-
- return sb.toString();
- }
-
- /**
- * USB hotplug event listener to register, unregister and detect when USB devices are added or removed.
+ * USB hotplug support to detect when USB tuners are added or removed. jlibusb reports changes as they happen
+ * where libusb supports hotplug (Linux and macOS) and polls the USB device list elsewhere (Windows).
*
- * Note: hotplug is not supported on all platforms.
+ * Tuners already connected when detection starts are reported as arrivals too. Those were added during discovery
+ * at startup and are ignored here because the tuner model already has a tuner at that bus and port.
*/
- public class HotplugEventSupport implements HotplugCallback
+ public class HotplugEventSupport implements UsbHotplugListener
{
- private static final int HOTPLUG_CONTINUE_EVENT_SUPPORT = 0;
- private HotplugCallbackHandle mHotplugCallbackHandle;
- private ScheduledFuture> mEventProcessorFuture;
+ private UsbHotplug mUsbHotplug;
/**
- * LibUsb hotplug event notification
- * @param context used for hotplug event registration
- * @param device that arrived or left
- * @param event value, arrived event or left event
- * @param userData not used
- * @return
- */
- @Override
- public int processEvent(Context context, Device device, int event, Object userData)
- {
- int bus = LibUsb.getBusNumber(device);
- int port = LibUsb.getPortNumber(device);
-
- if(port > 0)
- {
- String portAddress = getPortAddress(device);
-
- switch(event)
- {
- case LibUsb.HOTPLUG_EVENT_DEVICE_ARRIVED:
- DeviceDescriptor deviceDescriptor = new DeviceDescriptor();
- int status = LibUsb.getDeviceDescriptor(device, deviceDescriptor);
-
- if(status == LibUsb.SUCCESS)
- {
- TunerClass tunerClass = TunerClass.lookup(deviceDescriptor.idVendor(), deviceDescriptor.idProduct());
-
- if(tunerClass.isSupportedUsbTuner())
- {
- mLog.info("Tuner plug-in detected at USB Bus [" + bus + "] Port [" + port +
- "] Tuner Class [" + tunerClass + "]");
- ChannelizerType channelizerType = mUserPreferences.getTunerPreference().getChannelizerType();
- DiscoveredUSBTuner discoveredUSBTuner = new DiscoveredUSBTuner(tunerClass, bus,
- portAddress, channelizerType);
-
- if(tunerClass.isFuncubeTuner())
- {
- //Funcube tuners take a few moments to init the sound card interface. Delay adding
- //the tuner so that it can be started correctly.
- ThreadPool.SCHEDULED.schedule(() ->
- {
- addUsbTuner(discoveredUSBTuner);
- }, 2, TimeUnit.SECONDS);
- }
- else
- {
- addUsbTuner(discoveredUSBTuner);
- }
- }
- }
- break;
- case LibUsb.HOTPLUG_EVENT_DEVICE_LEFT:
- DiscoveredTuner removed = removeUsbTuner(bus, portAddress);
-
- if(removed != null)
- {
- mLog.info("Tuner Unplugged: " + removed.getId());
- }
- break;
- }
- }
-
- return HOTPLUG_CONTINUE_EVENT_SUPPORT;
- }
-
- /**
- * Registers for hotplug event notifications, if the capability is supported on this platform and starts an
- * event processing timer to process hotplug events.
+ * Starts hotplug detection of supported USB tuners.
*/
public void start()
{
- if(LibUsb.hasCapability(LibUsb.CAP_HAS_HOTPLUG))
+ try
{
- mHotplugCallbackHandle = new HotplugCallbackHandle();
- int events = LibUsb.HOTPLUG_EVENT_DEVICE_ARRIVED | LibUsb.HOTPLUG_EVENT_DEVICE_LEFT;
- int flags = LibUsb.HOTPLUG_ENUMERATE;
- int vendorId = LibUsb.HOTPLUG_MATCH_ANY;
- int productId = LibUsb.HOTPLUG_MATCH_ANY;
- int deviceClass = LibUsb.HOTPLUG_MATCH_ANY;
-
- mLog.info("LibUsb - starting HotPlug detection");
- int status = LibUsb.hotplugRegisterCallback(mLibUsbApplicationContext, events, flags, vendorId,
- productId, deviceClass, this, "sdrtrunk hotplug support", mHotplugCallbackHandle);
-
- if(status == LibUsb.SUCCESS)
- {
- Runnable eventHandler = () -> LibUsb.handleEvents(mLibUsbApplicationContext);
- mEventProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(eventHandler,
- 0, 1, TimeUnit.SECONDS);
- }
- else
- {
- mHotplugCallbackHandle = null;
- mLog.info("LibUsb - unable to register device hotplug listener - " + LibUsb.errorName(status));
- }
+ mUsbHotplug = UsbHotplug.start(device -> getSupportedTunerClass(device) != null, this);
+ mLog.info("LibUsb - started USB hotplug detection (" +
+ (mUsbHotplug.mode() == UsbHotplug.Mode.NATIVE ? "native" : "polling") + ")");
}
- else
+ catch(UsbException | RuntimeException e)
{
- mLog.info("LibUsb Hotplug event notification Is Not Supported on this platform.");
+ mLog.error("LibUsb - unable to start USB hotplug detection", e);
+ mUsbHotplug = null;
}
}
/**
- * Unregisters from hotplug event notifications and stops the event processing timer
+ * Stops hotplug detection. Waits for an arrival or removal that is being processed to finish.
*/
public void stop()
{
- if(mHotplugCallbackHandle != null)
+ if(mUsbHotplug != null)
{
- LibUsb.hotplugDeregisterCallback(mLibUsbApplicationContext, mHotplugCallbackHandle);
- mHotplugCallbackHandle = null;
+ mUsbHotplug.close();
+ mUsbHotplug = null;
}
+ }
- if(mEventProcessorFuture != null)
+ /**
+ * A supported USB tuner was connected. Called on the jlibusb hotplug dispatch thread.
+ * @param device that arrived
+ */
+ @Override
+ public void arrived(UsbDeviceInfo device)
+ {
+ try
{
- mEventProcessorFuture.cancel(true);
- mEventProcessorFuture = null;
+ DiscoveredUSBTuner discoveredUSBTuner = createDiscoveredTuner(device);
+
+ if(discoveredUSBTuner == null ||
+ mDiscoveredTunerModel.hasUsbTuner(discoveredUSBTuner.getBus(), discoveredUSBTuner.getPortAddress()))
+ {
+ return;
+ }
+
+ mLog.info("Tuner plug-in detected at USB Bus [" + discoveredUSBTuner.getBus() + "] Port [" +
+ discoveredUSBTuner.getPortAddress() + "] Tuner Class [" + discoveredUSBTuner.getTunerClass() + "]");
+
+ if(discoveredUSBTuner.getTunerClass().isFuncubeTuner())
+ {
+ //Funcube tuners take a few moments to init the sound card interface. Delay adding
+ //the tuner so that it can be started correctly.
+ ThreadPool.SCHEDULED.schedule(() -> addUsbTuner(discoveredUSBTuner), 2, TimeUnit.SECONDS);
+ }
+ else
+ {
+ addUsbTuner(discoveredUSBTuner);
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error adding USB tuner after plug-in at USB Bus [" + device.busNumber() + "] Port [" +
+ UsbPortAddress.of(device) + "]", e);
+ }
+ }
+
+ /**
+ * A supported USB tuner was disconnected. Called on the jlibusb hotplug dispatch thread.
+ * @param device that left
+ */
+ @Override
+ public void left(UsbDeviceInfo device)
+ {
+ try
+ {
+ DiscoveredTuner removed = removeUsbTuner(device.busNumber(), UsbPortAddress.of(device));
+
+ if(removed != null)
+ {
+ mLog.info("Tuner Unplugged: " + removed.getId());
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error removing USB tuner after unplug at USB Bus [" + device.busNumber() + "] Port [" +
+ UsbPortAddress.of(device) + "]", e);
}
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/UsbDeviceLookup.java b/src/main/java/io/github/dsheirer/source/tuner/manager/UsbDeviceLookup.java
new file mode 100644
index 00000000..be62c0e6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/manager/UsbDeviceLookup.java
@@ -0,0 +1,195 @@
+/*
+ * *****************************************************************************
+ * 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.manager;
+
+import io.github.dsheirer.libusb.LibUsb;
+import io.github.dsheirer.libusb.UsbContext;
+import io.github.dsheirer.libusb.UsbDeviceHandle;
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import io.github.dsheirer.libusb.UsbException;
+import io.github.dsheirer.source.SourceException;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Finds the USB device at a tuner's bus and port address and reads its USB identity through jlibusb.
+ *
+ * The tuner libraries open a device by serial number or by index rather than by bus and port, so a tuner controller
+ * uses this to identify the device that the tuner manager discovered at a bus and port before handing it to its
+ * library. Devices are closed again before returning so that the library can open them.
+ */
+public final class UsbDeviceLookup
+{
+ private static final Logger mLog = LoggerFactory.getLogger(UsbDeviceLookup.class);
+
+ /**
+ * libusb error code when the user doesn't have permission to open the device (LIBUSB_ERROR_ACCESS)
+ */
+ public static final int ERROR_ACCESS = -3;
+
+ /**
+ * libusb error code when another program or driver has claimed the interface (LIBUSB_ERROR_BUSY)
+ */
+ public static final int ERROR_BUSY = -6;
+
+ private UsbDeviceLookup()
+ {
+ }
+
+ /**
+ * Creates a libusb context. Close it when done.
+ * @throws SourceException if libusb can't be initialized
+ */
+ public static UsbContext createContext() throws SourceException
+ {
+ try
+ {
+ return UsbContext.create();
+ }
+ catch(UsbException e)
+ {
+ throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(e.code()));
+ }
+ }
+
+ /**
+ * Lists the USB devices, in libusb device list order.
+ * @param usb context
+ * @throws SourceException if the device list isn't available
+ */
+ public static List devices(UsbContext usb) throws SourceException
+ {
+ try
+ {
+ return usb.devices();
+ }
+ catch(UsbException e)
+ {
+ throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(e.code()));
+ }
+ }
+
+ /**
+ * Indicates if the device is at the bus and port address.
+ * @param device to test
+ * @param bus number
+ * @param portAddress in dotted notation, such as 2.3
+ */
+ public static boolean isAt(UsbDeviceInfo device, int bus, String portAddress)
+ {
+ return device.busNumber() == bus && !device.portNumbers().isEmpty() && portAddress != null &&
+ portAddress.equals(UsbPortAddress.of(device));
+ }
+
+ /**
+ * Finds the device at the bus and port address.
+ * @param usb context
+ * @param bus number
+ * @param portAddress in dotted notation, such as 2.3
+ * @throws SourceException if no device is at that bus and port
+ */
+ public static UsbDeviceInfo find(UsbContext usb, int bus, String portAddress) throws SourceException
+ {
+ for(UsbDeviceInfo device: devices(usb))
+ {
+ if(isAt(device, bus, portAddress))
+ {
+ return device;
+ }
+ }
+
+ throw new SourceException("Couldn't find USB device at bus [" + bus + "] port [" + portAddress + "]");
+ }
+
+ /**
+ * Finds the device at the bus and port address.
+ * @param bus number
+ * @param portAddress in dotted notation, such as 2.3
+ * @throws SourceException if no device is at that bus and port
+ */
+ public static UsbDeviceInfo find(int bus, String portAddress) throws SourceException
+ {
+ try(UsbContext usb = createContext())
+ {
+ return find(usb, bus, portAddress);
+ }
+ }
+
+ /**
+ * Opens the device. Close the handle before the context.
+ * @param usb context
+ * @param device to open
+ * @throws SourceException if the device can't be opened, logging the likely cause
+ */
+ public static UsbDeviceHandle open(UsbContext usb, UsbDeviceInfo device) throws SourceException
+ {
+ try
+ {
+ return usb.open(device);
+ }
+ catch(UsbException e)
+ {
+ if(e.code() == ERROR_ACCESS)
+ {
+ mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
+ throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
+ }
+
+ mLog.error("Can't open USB tuner - check driver or Linux udev rules");
+ throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(e.code()));
+ }
+ }
+
+ /**
+ * Reads the device's USB serial string.
+ * @param usb context
+ * @param device to read
+ * @return serial, or null if the device doesn't have a serial string or it can't be read
+ * @throws SourceException if the device has a serial string but can't be opened
+ */
+ public static String readSerialNumber(UsbContext usb, UsbDeviceInfo device) throws SourceException
+ {
+ if(device.serialNumberIndex() == 0)
+ {
+ return null;
+ }
+
+ try(UsbDeviceHandle handle = open(usb, device))
+ {
+ return handle.readString(device.serialNumberIndex());
+ }
+ }
+
+ /**
+ * Reads the USB serial string of the device at the bus and port address.
+ * @param bus number
+ * @param portAddress in dotted notation, such as 2.3
+ * @return serial, or null if the device doesn't have a serial string or it can't be read
+ * @throws SourceException if no device is at that bus and port, or it can't be opened
+ */
+ public static String readSerialNumber(int bus, String portAddress) throws SourceException
+ {
+ try(UsbContext usb = createContext())
+ {
+ return readSerialNumber(usb, find(usb, bus, portAddress));
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/UsbPortAddress.java b/src/main/java/io/github/dsheirer/source/tuner/manager/UsbPortAddress.java
new file mode 100644
index 00000000..26dba8e4
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/manager/UsbPortAddress.java
@@ -0,0 +1,53 @@
+/*
+ * *****************************************************************************
+ * 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.manager;
+
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Formats the USB port address of a device: the port numbers from the root hub through any intermediate hubs, in
+ * dotted notation such as 2.3. Together with the bus number it identifies the physical USB socket of a tuner.
+ *
+ * The tuner manager discovers tuners at this address and the tuner controllers find their device again by it, so it
+ * is formatted in one place.
+ */
+public final class UsbPortAddress
+{
+ private UsbPortAddress()
+ {
+ }
+
+ /**
+ * Port address of a device.
+ * @param device to inspect
+ * @return formatted address, or an empty string for a root hub
+ */
+ public static String of(UsbDeviceInfo device)
+ {
+ return format(device.portNumbers());
+ }
+
+ private static String format(List ports)
+ {
+ return ports.stream().map(String::valueOf).collect(Collectors.joining("."));
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java
index 046b7610..5cae76b6 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java
@@ -18,6 +18,10 @@
*/
package io.github.dsheirer.source.tuner.rtl;
+import io.github.dsheirer.libusb.UsbContext;
+import io.github.dsheirer.libusb.UsbDeviceHandle;
+import io.github.dsheirer.libusb.UsbDeviceInfo;
+import io.github.dsheirer.libusb.UsbException;
import io.github.dsheirer.buffer.ByteNativeBufferFactory;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.rtlsdr.DeviceInfo;
@@ -32,7 +36,7 @@ import io.github.dsheirer.source.tuner.TunerClass;
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.source.tuner.manager.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.fc0012.FC0012TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013TunerConfiguration;
@@ -44,12 +48,6 @@ import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.usb4java.Context;
-import org.usb4java.Device;
-import org.usb4java.DeviceDescriptor;
-import org.usb4java.DeviceHandle;
-import org.usb4java.DeviceList;
-import org.usb4java.LibUsb;
/**
* RTL-2832 tuner controller, implemented with the jRTL library (librtlsdr from rtl-sdr-blog).
@@ -328,73 +326,40 @@ public class RTL2832TunerController extends TunerController
*/
private UsbIdentity readUsbIdentity() throws SourceException
{
- Context context = new Context();
- int status = LibUsb.init(context);
-
- if(status != LibUsb.SUCCESS)
+ try(UsbContext usb = UsbDeviceLookup.createContext())
{
- throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
- }
+ //RTL-2832 devices that precede this controller's device in the device list
+ List earlierDevices = new ArrayList<>();
- try
- {
- DeviceList deviceList = new DeviceList();
- int count = LibUsb.getDeviceList(context, deviceList);
-
- if(count < 0)
+ for(UsbDeviceInfo device : UsbDeviceLookup.devices(usb))
{
- throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
- }
-
- try
- {
- //RTL-2832 devices that precede this controller's device in the device list
- List earlierDevices = new ArrayList<>();
- List earlierDescriptors = new ArrayList<>();
-
- for(Device device : deviceList)
+ if(TunerClass.lookup((short)device.vendorId(), (short)device.productId()) != TunerClass.RTL2832)
{
- DeviceDescriptor descriptor = new DeviceDescriptor();
+ continue;
+ }
- if(LibUsb.getDeviceDescriptor(device, descriptor) != LibUsb.SUCCESS ||
- TunerClass.lookup(descriptor.idVendor(), descriptor.idProduct()) != TunerClass.RTL2832)
+ if(UsbDeviceLookup.isAt(device, mBus, mPortAddress))
+ {
+ mUsbSerialNumberIndex = device.serialNumberIndex();
+ String serial = readSerial(usb, device, true);
+ int serialPosition = 0;
+
+ if(serial != null)
{
- continue;
- }
-
- if(LibUsb.getBusNumber(device) == mBus && LibUsb.getPortNumber(device) > 0 &&
- mPortAddress != null && mPortAddress.equals(TunerManager.getPortAddress(device)))
- {
- mUsbSerialNumberIndex = 0xFF & descriptor.iSerialNumber();
- String serial = readSerial(device, descriptor, true);
- int serialPosition = 0;
-
- if(serial != null)
+ for(UsbDeviceInfo earlierDevice : earlierDevices)
{
- for(int x = 0; x < earlierDevices.size(); x++)
+ if(serial.equals(readSerial(usb, earlierDevice, false)))
{
- if(serial.equals(readSerial(earlierDevices.get(x), earlierDescriptors.get(x), false)))
- {
- serialPosition++;
- }
+ serialPosition++;
}
}
-
- return new UsbIdentity(serial, serialPosition, earlierDevices.size());
}
- earlierDevices.add(device);
- earlierDescriptors.add(descriptor);
+ return new UsbIdentity(serial, serialPosition, earlierDevices.size());
}
+
+ earlierDevices.add(device);
}
- finally
- {
- LibUsb.freeDeviceList(deviceList, true);
- }
- }
- finally
- {
- LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
@@ -402,51 +367,32 @@ public class RTL2832TunerController extends TunerController
/**
* Reads the device's USB serial string.
+ * @param usb context
* @param device to read
- * @param descriptor for the device
* @param required true to throw an exception when the device can't be opened, false to return null
* @return serial or null if the device doesn't have a serial string
* @throws SourceException if the device is required and it can't be opened.
*/
- private static String readSerial(Device device, DeviceDescriptor descriptor, boolean required) throws SourceException
+ private static String readSerial(UsbContext usb, UsbDeviceInfo device, boolean required) throws SourceException
{
- if(descriptor.iSerialNumber() == 0)
+ if(!required)
{
- return null;
- }
-
- DeviceHandle handle = new DeviceHandle();
- int status = LibUsb.open(device, handle);
-
- if(status != LibUsb.SUCCESS)
- {
- if(!required)
+ if(device.serialNumberIndex() == 0)
{
return null;
}
- if(status == LibUsb.ERROR_ACCESS)
+ try(UsbDeviceHandle handle = usb.open(device))
{
- mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) check udev rules");
- throw new SourceException("access denied - if using linux, install udev rules");
+ return handle.readString(device.serialNumberIndex());
+ }
+ catch(UsbException e)
+ {
+ return null;
}
-
- mLog.error("Can't open USB tuner - check driver or Linux udev rules");
- throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
}
- try
- {
- return LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
- }
- catch(RuntimeException e)
- {
- return null;
- }
- finally
- {
- LibUsb.close(handle);
- }
+ return UsbDeviceLookup.readSerialNumber(usb, device);
}
/**
diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
index e53f8ae7..4ce12bb3 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
@@ -320,7 +320,7 @@ public class DiscoveredTunerModel extends AbstractTableModel implements Listener
{
DiscoveredTuner discoveredTuner = mDiscoveredTuners.stream()
.filter(tuner -> tuner instanceof DiscoveredUSBTuner usbTuner &&
- usbTuner.isAt(bus, portAddress)).findFirst().get();
+ usbTuner.isAt(bus, portAddress)).findFirst().orElse(null);
if(discoveredTuner != null)
{