From 006e382b349a15aaed8ed6334e3f498025fa8d16 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 8 Nov 2026 09:56:09 -0500 Subject: [PATCH] #2604 jLibUSB library integration (#2605) Co-authored-by: denny --- build.gradle | 27 +- settings.gradle | 9 + .../tuner/airspy/AirspyTunerController.java | 95 +--- .../airspy/hf/AirspyHfTunerController.java | 93 +--- .../tuner/bladerf/BladeRfTunerController.java | 47 +- .../dsheirer/source/tuner/fcd/FCDCommand.java | 2 +- .../source/tuner/fcd/FCDTunerController.java | 403 +++++++------- .../source/tuner/fcd/FCDUsbException.java | 50 ++ .../tuner/hackrf/HackRFTunerController.java | 109 +--- .../hydrasdr/HydraSdrTunerController.java | 95 +--- .../tuner/limesdr/LimeSdrTunerController.java | 114 +--- .../source/tuner/manager/TunerManager.java | 493 ++++++++---------- .../source/tuner/manager/UsbDeviceLookup.java | 195 +++++++ .../source/tuner/manager/UsbPortAddress.java | 53 ++ .../tuner/rtl/RTL2832TunerController.java | 126 ++--- .../source/tuner/ui/DiscoveredTunerModel.java | 2 +- 16 files changed, 788 insertions(+), 1125 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/source/tuner/fcd/FCDUsbException.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/UsbDeviceLookup.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/UsbPortAddress.java 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) {