Compare commits

..

1 Commits

Author SHA1 Message Date
denny
5d9d40836b #2566 P25P1 Adjacent Status Broadcast message update. 2026-11-08 04:51:47 -05:00
40 changed files with 1149 additions and 3419 deletions

View File

@@ -95,54 +95,66 @@ test {
dependencies {
// JUnit Tests
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation(platform("org.junit:junit-bom:6.0.1"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
//Jitpack imports
implementation 'com.github.dnault:libresample4j:master'
implementation 'com.github.DSheirer.jmbe:api:v2.1.1'
implementation 'com.github.DSheirer.jmbe:api:v2.1.0'
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.6.6'
implementation 'ch.qos.logback:logback-core:1.6.6'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.23.0'
implementation 'ch.qos.logback:logback-classic:1.6.3'
implementation 'ch.qos.logback:logback-core:1.5.38'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.4'
implementation 'com.fazecast:jSerialComm:2.11.4'
implementation 'com.formdev:flatlaf:3.8'
implementation 'com.formdev:flatlaf-intellij-themes:3.8' //Not imported - Theme.java loads these themes by class name
implementation 'com.formdev:flatlaf:3.7.2'
implementation 'com.formdev:flatlaf-intellij-themes:3.7.2'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation('com.github.wendykierp:JTransforms:3.2') {
exclude group: 'junit' //JLargeArrays 1.7 declares junit as a compile dependency
}
implementation 'com.google.code.gson:gson:2.15.0'
implementation 'com.google.guava:guava:33.8.0-jre'
implementation 'com.github.wendykierp:JTransforms:3.2'
implementation 'com.google.code.gson:gson:2.14.0'
implementation 'com.google.guava:guava:33.6.0-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:11.5.2'
implementation 'com.miglayout:miglayout-swing:11.4.3'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.22.0'
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 '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:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jsdrplay:3.15.3' //SDRplay RSP tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'org.apache.commons:commons-compress:1.28.0' //Not used directly - overrides the vulnerable 1.21 version from jarchivelib
implementation 'org.apache.commons:commons-lang3:3.21.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'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.14.1'
implementation 'org.apache.mina:mina-core:2.2.10'
implementation 'org.apache.mina:mina-http:2.2.10'
implementation 'org.controlsfx:controlsfx:11.2.6'
implementation 'org.apache.mina:mina-core:2.2.9'
implementation 'org.apache.mina:mina-http:2.2.9'
implementation 'org.controlsfx:controlsfx:11.2.3'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.20'
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'
}
def os = DefaultNativePlatform.currentOperatingSystem
@@ -233,6 +245,18 @@ String targetOsxX86_64 = 'osx-x86_64-v' + version
String targetWindowsX86_64 = 'windows-x86_64-v' + version
String targetWindowsAarch64 = 'windows-aarch64-v' + version
/**
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
* author's development environment, but can be changed to your environment if you want to use this build pattern.
*/
String jdk_base = '/home/denny/java_jdks/'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
/**
* Download URLs to download the JDK as part of the gradle build packaging process
*/
@@ -261,170 +285,19 @@ def configure(RuntimeZipTask rt, List<String> jvmArgs) {
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
//jdk.accessibility is used with assistive technologies like screen readers
//java.management for JVM resource monitoring
//java.base, java.logging, java.net.http, java.prefs, javafx.controls, javafx.swing - required by sdrtrunk (jdeps)
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management',
'java.base', 'java.logging', 'java.net.http', 'java.prefs', 'javafx.controls', 'javafx.swing')
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
//Use only the modules listed above. The runtime plugin's module auto-detection uses a bundled ASM library that
//can't read Java 27 class files and logs a 'Failed to scan' error for each sdrtrunk class. The jdeps module check
//on the runtime task verifies that this list is complete.
rt.extension.additive.set(false)
//Use auto-detected modules and 'add' any specified modules.
rt.extension.additive.set(true)
//Recommended options for reducing/shrinking runtime target image size
rt.extension.addOptions('--strip-debug', '--compress', 'zip-6', '--no-header-files', '--no-man-pages')
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages')
//Runtime images have auto appended version in image zip name. Override to include version in single target image.
// rt.extension.imageDir=layout.buildDirectory.dir('image')
rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip')))
}
/**
* Native library folders to keep in each runtime target image, keyed by the target name prefix. The tuner libraries
* (jRTL, jAirspy, etc.) package their native libraries under '<library>/native/<os>-<arch>/' and jSerialComm packages
* them under '<OS>/<arch>/'. All other operating system and architecture folders are removed from the target image.
*/
def nativeFolders = [
'linux-aarch64' : [tuner: 'linux-aarch64', serial: 'Linux/armv8_64'],
'linux-x86_64' : [tuner: 'linux-x86_64', serial: 'Linux/x86_64'],
'osx-aarch64' : [tuner: 'macos-aarch64', serial: 'OSX/aarch64'],
'osx-x86_64' : [tuner: 'macos-x86_64', serial: 'OSX/x86_64'],
'windows-aarch64': [tuner: 'windows-aarch64', serial: 'Windows/aarch64'],
'windows-x86_64' : [tuner: 'windows-x86_64', serial: 'Windows/x86_64']
]
/**
* Top level folders that jSerialComm uses for its per-OS native libraries.
*/
def serialOsFolders = ['Android', 'FreeBSD', 'Linux', 'OpenBSD', 'OSX', 'Solaris', 'Windows'] as Set
/**
* Rewrites the jar file without the native library folders for other operating systems and architectures. Fails the
* build if a jar that contains native libraries doesn't contain the folder for the target.
* @param jar file to rewrite
* @param keep native folders for the target (tuner and serial)
* @param serialOsFolders jSerialComm top level OS folders
* @return number of bytes removed from the jar
*/
long stripJar(File jar, Map<String,String> keep, Set<String> serialOsFolders) {
def tunerPattern = ~/^[^\/]+\/native\/([^\/]+)\/.*/
def serialPrefix = keep.serial + '/'
def removed = [] as Set
def hadTuner = false, keptTuner = false, hadSerial = false, keptSerial = false
File temp = new File(jar.parentFile, jar.name + '.tmp')
new java.util.zip.ZipFile(jar).withCloseable { zip ->
def entries = zip.entries().toList()
entries.each { e ->
def tuner = tunerPattern.matcher(e.name)
def top = e.name.contains('/') ? e.name.substring(0, e.name.indexOf('/')) : null
if(tuner.matches()) {
hadTuner = true
if(tuner.group(1) == keep.tuner) { keptTuner = true } else { removed << e.name }
}
else if(top in serialOsFolders) {
hadSerial = true
if(e.name.startsWith(serialPrefix)) { keptSerial = true } else { removed << e.name }
}
}
if(hadTuner && !keptTuner) {
throw new GradleException("Native strip: ${jar.name} has no native/${keep.tuner} folder")
}
if(hadSerial && !keptSerial) {
throw new GradleException("Native strip: ${jar.name} has no ${keep.serial} folder")
}
if(!removed.isEmpty()) {
new java.util.zip.ZipOutputStream(new FileOutputStream(temp)).withCloseable { out ->
entries.findAll { !removed.contains(it.name) }.each { e ->
def copy = new java.util.zip.ZipEntry(e)
if(copy.method == java.util.zip.ZipEntry.DEFLATED) {
copy.compressedSize = -1
}
out.putNextEntry(copy)
zip.getInputStream(e).withCloseable { out << it }
out.closeEntry()
}
}
}
}
if(removed.isEmpty()) {
return 0
}
long before = jar.length()
java.nio.file.Files.move(temp.toPath(), jar.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING)
return before - jar.length()
}
/**
* Removes the native libraries for other operating systems and architectures from the jars in each runtime target
* image, before the runtimeZip tasks package the images.
*/
tasks.named('runtime') {rt ->
rt.doLast {
rt.targetPlatforms.get().keySet().each { target ->
def key = nativeFolders.keySet().find { target.startsWith(it + '-') }
if(key == null) {
throw new GradleException("Native strip: no native folder mapping for runtime target [${target}]")
}
def lib = new File(rt.imageDir.asFile, "${project.name}-${target}/lib")
long total = 0
lib.listFiles({ File f -> f.name.endsWith('.jar') } as FileFilter).each { jar ->
total += stripJar(jar, nativeFolders[key], serialOsFolders)
}
logger.lifecycle("Native strip: removed ${String.format('%.1f', total / 1048576)} MB of other " +
"platform native libraries from [${target}]")
}
}
}
/**
* Verifies that each runtime target image contains every module that the sdrtrunk jar requires, as reported by jdeps.
* This guards against the runtime plugin's module auto-detection silently missing modules (e.g. when it can't parse
* class files compiled for a newer Java version).
*/
def jdepsHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
def mainJar = tasks.named('jar').flatMap { it.archiveFile }
tasks.named('runtime') {rt ->
rt.doLast {
def jdeps = new File(jdepsHome.get(), 'bin/' + (os.isWindows() ? 'jdeps.exe' : 'jdeps')).absolutePath
def process = new ProcessBuilder(jdeps, '--multi-release', java.toolchain.languageVersion.get().toString(),
'--ignore-missing-deps', '--print-module-deps', '-q', mainJar.get().asFile.absolutePath)
.redirectErrorStream(true).start()
def output = process.inputStream.text.trim()
if(process.waitFor() != 0) {
throw new GradleException("Module check: jdeps failed: ${output}")
}
def required = output.split(',')*.trim().findAll { it } as Set
rt.targetPlatforms.get().keySet().each { target ->
def release = new File(rt.imageDir.asFile, "${project.name}-${target}/release")
def match = release.text =~ /(?m)^MODULES="([^"]*)"/
if(!match.find()) {
throw new GradleException("Module check: no MODULES entry in ${release}")
}
def missing = required - (match.group(1).split(' ') as Set)
if(missing) {
throw new GradleException("Module check: runtime image [${target}] is missing required modules " +
"${missing.sort()} - add them to addModules() in build.gradle")
}
}
logger.lifecycle("Module check: runtime images contain all modules required by sdrtrunk: ${required.sort()}")
}
}
/**
* Creates a release package for the current operating system.
*
@@ -481,12 +354,22 @@ tasks.register('runtimeZipCurrent', RuntimeZipTask) {rt ->
*/
tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
//Windows aarch64
rt.extension.targetPlatform(targetWindowsAarch64) {
jdkHome = jdkDownload(jdk_download_windows_aarch64)
if(file(jdk_windows_aarch64).exists()) {
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
}
else {
rt.extension.targetPlatform(targetWindowsAarch64) {
jdkHome = jdkDownload(jdk_download_windows_aarch64)
}
}
//Windows x86-64
rt.extension.targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload(jdk_download_windows_x86_64)
if(file(jdk_windows_x86_64).exists()) {
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
}
else {
rt.extension.targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload(jdk_download_windows_x86_64)
}
}
configure(rt, jvmArgsWindows)
@@ -503,20 +386,40 @@ tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
*/
tasks.register('runtimeZipOthers', RuntimeZipTask) {rt ->
//Linux aarch64
rt.extension.targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload(jdk_download_linux_aarch64)
if(file(jdk_linux_aarch64).exists()) {
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, [])
}
else {
rt.extension.targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload(jdk_download_linux_aarch64)
}
}
//Linux x86-64
rt.extension.targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload(jdk_download_linux_x86_64)
if(file(jdk_linux_x86_64).exists()) {
rt.extension.targetPlatform(targetLinuxX86_64, jdk_linux_x86_64, [])
}
else {
rt.extension.targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload(jdk_download_linux_x86_64)
}
}
//OSX aarch64
rt.extension.targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload(jdk_download_osx_aarch64)
if(file(jdk_osx_aarch64).exists()) {
rt.extension.targetPlatform(targetOsxAarch64, jdk_osx_aarch64, [])
}
else {
rt.extension.targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload(jdk_download_osx_aarch64)
}
}
//OSX x86-64
rt.extension.targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload(jdk_download_osx_x86_64)
if(file(jdk_osx_x86_64).exists()) {
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
}
else {
rt.extension.targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload(jdk_download_osx_x86_64)
}
}
configure(rt, jvmArgsLinux)

View File

@@ -28,4 +28,4 @@
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=1.0.0-beta-1
projectVersion=0.6.2-beta-1

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.9.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500

52
gradlew.bat vendored
View File

@@ -26,33 +26,6 @@
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
@rem Catch executions from older scripts and ensure they exit cleanly.
@rem This can be removed once we can be reasonably confident that few people
@rem will be migrating directly to this new wrapper.
goto afterSafetyNet
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto exitWithErrorLevel
:afterSafetyNet
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
@@ -72,14 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -87,14 +59,13 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:execute
@rem Setup the command line
@@ -104,9 +75,8 @@ goto exitWithErrorLevel
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

View File

@@ -7,7 +7,7 @@ dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = 'https://jitpack.io' }
maven { url 'https://jitpack.io' }
//jAirspy is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
@@ -33,15 +33,6 @@ 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'
@@ -58,14 +49,6 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
}
//jLimeSDR is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jLimeSDR/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jlimesdr' }
}
//jsdrplay is published as a GitHub release. The jar does not contain the SDRplay API native library, which
//users install separately from sdrplay.com
ivy {

View File

@@ -257,20 +257,6 @@ public abstract class C4FMSymbolProcessor
return true;
}
/**
* Maximum bit errors allowed in a detected sync pattern while acquiring (ie not synchronized), measured from the
* symbol decisions at the optimized symbol timing. Sync detections with more bit errors are rejected. Bit errors
* include symbol magnitude (ie +3 vs +1) errors, so this rejects noise where the symbol signs correlate with the
* sync pattern but the symbol magnitudes are inconsistent. Once synchronized, the limit isn't applied since the
* sync detection timing is known and rejecting a valid sync detection costs the burst. Default implementation
* imposes no limit.
* @return maximum sync pattern bit error count while acquiring.
*/
public int getSyncAcquisitionMaxBitErrors()
{
return Integer.MAX_VALUE;
}
/**
* Number of consecutive sync periods (capped at 3) where a sync was detected within the max message length.
*/
@@ -959,14 +945,12 @@ public abstract class C4FMSymbolProcessor
}
/**
* Indicates if the optimization score is high quality and, while acquiring, the sync pattern bit errors don't
* exceed the max.
* Indicates if this correction has a valid NID or the optimization (or detection) score is high quality.
* @return true if valid.
*/
public boolean isValid()
{
return mOptimizationScore > getSyncThresholdEqualized() &&
(mSynchronized || mCorrectedBitCount <= getSyncAcquisitionMaxBitErrors());
return mOptimizationScore > getSyncThresholdEqualized();
}
}

View File

@@ -45,7 +45,6 @@ import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
/**
@@ -576,17 +575,7 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
@Override
public void mouseWheelMoved(MouseWheelEvent e)
{
//Swing delivers mouse wheel events to disabled components, so check enabled state explicitly so that
//the wheel can't bypass the lock when the tuner is providing channels.
if(isEnabled())
{
set(mValue - e.getWheelRotation());
}
else if(getParent() != null)
{
//Pass the event up so that an enclosing scroll pane can still scroll while this control is locked
getParent().dispatchEvent(SwingUtilities.convertMouseEvent(Digit.this, e, getParent()));
}
set(mValue - e.getWheelRotation());
}
}
}

View File

@@ -23,7 +23,6 @@ import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.gui.control.AccessibleToggleSwitch;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.application.Platform;
@@ -69,26 +68,6 @@ public abstract class AbstractBroadcastEditor<T extends BroadcastConfiguration>
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(getSaveButton(), getResetButton());
/**
* Allow column 1 (Primary input fields) to expand with editor width
*/
for(Node node : getEditorPane().getChildren())
{
Integer column = GridPane.getColumnIndex(node);
if(column != null && column == 1)
{
if(node instanceof IntegerTextField)
{
GridPane.setFillWidth(node, false);
}
else if(node instanceof TextField)
{
GridPane.setHgrow(node, Priority.ALWAYS);
}
}
}
HBox editorBox = new HBox();
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
editorBox.getChildren().addAll(getEditorPane(), buttonBox);

View File

@@ -298,10 +298,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
if(message instanceof VoiceMessage voice)
{
if(isValid(voice))
{
processVoice(voice);
}
processVoice(voice);
}
else if(message instanceof DataMessage data)
{

View File

@@ -66,12 +66,6 @@ public class DMRSymbolProcessor extends C4FMSymbolProcessor
private static final float SYNC_THRESHOLD_EQUALIZED = 70;
//Once synchronized, sync detections are only allowed within this many symbols of a burst boundary.
private static final int SYNC_WINDOW_SYMBOLS = 2;
//Maximum bit errors (of 48) in a detected sync pattern while acquiring. Testing: all false sync detections in
//600 seconds of noise occurred while acquiring, with 7+ (mostly 12+) bit errors, and a max of 8 eliminated them
//(245 bursts to 0) at a cost of < 0.6% valid data bursts on clean recordings and 0.3 - 5.5% on multipath
//recordings with injected noise, where valid link control improved. A max of 10 leaks noise false syncs and 6
//costs more multipath data bursts. Applying the max once synchronized cost 3 - 20% of valid bursts.
private static final int SYNC_ACQUISITION_MAX_BIT_ERRORS = 8;
private final DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
private final DMRMessageFramer mMessageFramer;
@@ -148,12 +142,6 @@ public class DMRSymbolProcessor extends C4FMSymbolProcessor
return Math.min(offset, BURST_DIBIT_LENGTH - offset) <= SYNC_WINDOW_SYMBOLS;
}
@Override
public int getSyncAcquisitionMaxBitErrors()
{
return SYNC_ACQUISITION_MAX_BIT_ERRORS;
}
@Override
public boolean isDynamicBalanceTrackingRequired()
{

View File

@@ -105,13 +105,6 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
{
if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
{
//Ignore voice messages where none of the AMBE voice frames pass error detection, so that invalid voice
//messages (ie false sync detections) don't produce audio or establish the encryption state.
if(message instanceof VoiceMessage voiceMessage && !voiceMessage.isValid())
{
return;
}
//Attempt to set the audio encryption state from certain types of messages
if(!mEncryptedCallStateEstablished)
{

View File

@@ -1,207 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.voice;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Error detection for the three AMBE+2 (3600 x 2450) voice frames carried in a DMR voice burst.
*
* Each 72-bit voice frame carries an extended Golay(24,12) protected C0 vector and a Golay(23,12) protected C1 vector
* that is scrambled with a pseudo-random sequence seeded by the 12 data bits of C0. The C2 and C3 vectors are not
* protected. Golay(23,12) is a perfect code where every 23-bit word decodes to a codeword within 3 bit errors, so a
* frame is only considered valid when the combined C0 and C1 error count is low, which random (ie noise) bits rarely
* achieve.
*
* Vector bit indices and the C1 scrambling sequence are from the JMBE AMBEFrame decoder. Burst bit indices of each
* voice frame match VoiceMessage.getAMBEFrames(): frames 1 and 3 are contiguous and frame 2 is split around the 48-bit
* sync/embedded signalling field in the middle of the burst.
*/
public class AMBEFrameValidator
{
/**
* Maximum combined C0 and C1 bit errors for a valid voice frame. Random 47-bit sequences have a combined error
* count of 2 or less with a probability of ~1.3e-4.
*/
public static final int MAX_ERRORS = 2;
private static final int[] VECTOR_C0 = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 1, 5,
9, 13, 17, 21};
private static final int[] VECTOR_C1 = {25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 2, 6, 10, 14, 18, 22, 26,
30, 34, 38, 42};
private static final int[] FRAME_OFFSETS = {24, 96, 216};
private static final int GOLAY_23_GENERATOR = 0xC75;
private static final int[][] C0_INDICES = new int[3][VECTOR_C0.length];
private static final int[][] C1_INDICES = new int[3][VECTOR_C1.length];
//Minimum weight error pattern for each Golay(23,12) syndrome (perfect code: all syndromes have weight <= 3)
private static final int[] ERROR_PATTERNS = new int[2048];
//C1 descrambling sequence for each 12-bit C0 data (u0) seed value, MSB first
private static final int[] SCRAMBLE_SEQUENCES = new int[4096];
static
{
for(int frame = 0; frame < 3; frame++)
{
for(int x = 0; x < VECTOR_C0.length; x++)
{
C0_INDICES[frame][x] = getBurstIndex(frame, VECTOR_C0[x]);
}
for(int x = 0; x < VECTOR_C1.length; x++)
{
C1_INDICES[frame][x] = getBurstIndex(frame, VECTOR_C1[x]);
}
}
//Golay(23,12) is perfect: the 2048 error patterns of weight 0 - 3 map one-to-one onto the 2048 syndromes.
for(int i = 0; i < 23; i++)
{
ERROR_PATTERNS[getSyndrome(1 << i)] = 1 << i;
for(int j = i + 1; j < 23; j++)
{
ERROR_PATTERNS[getSyndrome((1 << i) | (1 << j))] = (1 << i) | (1 << j);
for(int k = j + 1; k < 23; k++)
{
int pattern = (1 << i) | (1 << j) | (1 << k);
ERROR_PATTERNS[getSyndrome(pattern)] = pattern;
}
}
}
for(int seed = 0; seed < 4096; seed++)
{
int pr = 16 * seed;
int sequence = 0;
for(int x = 0; x < 23; x++)
{
pr = (173 * pr + 13849) % 65536;
sequence = (sequence << 1) | (pr >= 32768 ? 1 : 0);
}
SCRAMBLE_SEQUENCES[seed] = sequence;
}
}
private AMBEFrameValidator()
{
}
/**
* Burst bit index for the voice frame bit index.
* @param frame index 0-2
* @param bit index 0-71 within the (interleaved) voice frame
* @return bit index within the 288-bit burst
*/
private static int getBurstIndex(int frame, int bit)
{
//Frame 2 is split around the 48-bit sync/embedded signalling field (burst bits 132 - 179)
if(frame == 1 && bit >= 36)
{
return FRAME_OFFSETS[frame] + bit + 48;
}
return FRAME_OFFSETS[frame] + bit;
}
/**
* Golay(23,12) syndrome for the 23-bit word where the MSB (bit 22) is the first codeword bit.
*/
private static int getSyndrome(int word)
{
for(int bit = 22; bit >= 11; bit--)
{
if((word & (1 << bit)) != 0)
{
word ^= GOLAY_23_GENERATOR << (bit - 11);
}
}
return word;
}
/**
* Extracts the indexed bits from the message as an integer value, with the first index as the MSB.
*/
private static int getValue(BinaryMessage message, int[] indices)
{
int value = 0;
for(int index : indices)
{
value = (value << 1) | (message.get(index) ? 1 : 0);
}
return value;
}
/**
* Combined C0 and C1 bit error count for the voice frame, where C0 is decoded first so that the corrected C0 data
* bits can seed the C1 descrambling sequence. C0 errors include the extended Golay parity bit, so an even weight
* C0 error pattern (ie 4 errors) that Golay(23,12) mis-corrects is counted as 4 errors.
* @param message containing the 288-bit voice burst
* @param frame index 0-2
* @return combined C0 and C1 bit error count.
*/
public static int getErrorCount(BinaryMessage message, int frame)
{
int c0 = getValue(message, C0_INDICES[frame]);
int golay = c0 >>> 1;
int pattern = ERROR_PATTERNS[getSyndrome(golay)];
int corrected = golay ^ pattern;
int errors = Integer.bitCount(pattern);
//Extended Golay(24,12) parity bit provides even parity across the 24-bit codeword
if(((Integer.bitCount(corrected) + (c0 & 1)) & 1) != 0)
{
errors++;
}
//Short circuit when C0 alone exceeds the max error count.
if(errors > MAX_ERRORS)
{
return errors;
}
int c1 = getValue(message, C1_INDICES[frame]) ^ SCRAMBLE_SEQUENCES[corrected >>> 11];
return errors + Integer.bitCount(ERROR_PATTERNS[getSyndrome(c1)]);
}
/**
* Indicates if at least one of the three voice frames in the burst is valid, checking each frame in turn until a
* valid frame is found.
* @param message containing the 288-bit voice burst
* @return true if at least one voice frame has a combined C0 and C1 error count within the max error count.
*/
public static boolean isValid(BinaryMessage message)
{
for(int frame = 0; frame < 3; frame++)
{
if(getErrorCount(message, frame) <= MAX_ERRORS)
{
return true;
}
}
return false;
}
}

View File

@@ -40,10 +40,9 @@ public abstract class VoiceMessage extends DMRBurst
public static int[] FRAME_1_IV_FRAGMENT = new int[]{95, 91, 87, 83};
public static int[] FRAME_2_IV_FRAGMENT = new int[]{215, 211, 207, 203};
public static int[] FRAME_3_IV_FRAGMENT = new int[]{287, 283, 279, 275};
private Boolean mValid;
/**
* DMR message frame. This message comprises a 24-bit prefix and a 264-bit message frame. Outbound base
* DMR message frame. This message is comprised of a 24-bit prefix and a 264-bit message frame. Outbound base
* station frames transmit a Common Announcement Channel (CACH) in the 24-bit prefix, whereas Mobile inbound frames
* do not use the 24-bit prefix.
*
@@ -74,8 +73,8 @@ public abstract class VoiceMessage extends DMRBurst
{
frame_2[i] = getMessage().getByte(96 + i * 8);
}
// 4 bits and 4 bits. Note: mask the shifted (signed) byte to prevent sign extension into the upper 4 bits.
frame_2[4] = (byte)((getMessage().getByte(128) & 0xF0) | ((getMessage().getByte(180) >> 4) & 0x0F));
// 4 bits and 4 bits
frame_2[4] = (byte)((getMessage().getByte(128) & 0xF0) | (getMessage().getByte(180) >> 4));
// copy last 4 byte
for(int i = 0; i < 4; i++)
{
@@ -110,20 +109,10 @@ public abstract class VoiceMessage extends DMRBurst
return fragments;
}
/**
* Indicates if the voice burst is valid, meaning that at least one of the three AMBE voice frames passes error
* detection (see AMBEFrameValidator). Frames are checked in turn until a valid frame is found and the result is
* cached.
*/
@Override
public boolean isValid()
{
if(mValid == null)
{
mValid = AMBEFrameValidator.isValid(getMessage());
}
return mValid;
return true;
}
@Override

View File

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

View File

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

View File

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

View File

@@ -32,13 +32,19 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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).
@@ -206,17 +212,98 @@ 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 USB serial string is formatted as: AIRSPY SN:0123456789ABCDEF
* device by serial number. The device is closed again before returning so that libairspy can open it.
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() throws SourceException
{
String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
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
{
return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1).trim(), 16);
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);
}
catch(NullPointerException | NumberFormatException e)
{

View File

@@ -32,12 +32,18 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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
@@ -252,13 +258,94 @@ 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 USB serial string is formatted as: AIRSPYHF SN:0123456789ABCDEF
* device by serial number. The device is closed again before returning so that libairspyhf can open it.
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() throws SourceException
{
String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
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);
}
try
{

View File

@@ -45,13 +45,17 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.Context;
import org.usb4java.Device;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
/**
* bladeRF tuner controller, implemented with the jBladeRF library (libbladeRF).
@@ -372,7 +376,46 @@ public class BladeRfTunerController extends TunerController
*/
private int readUsbAddress() throws SourceException
{
return UsbDeviceLookup.find(mBus, mPortAddress).address();
Context context = new Context();
int status = LibUsb.init(context);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
try
{
DeviceList deviceList = new DeviceList();
int count = LibUsb.getDeviceList(context, deviceList);
if(count < 0)
{
throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
}
try
{
for(Device device : deviceList)
{
if(LibUsb.getBusNumber(device) == mBus && LibUsb.getPortNumber(device) > 0 &&
mPortAddress != null && mPortAddress.equals(TunerManager.getPortAddress(device)))
{
return LibUsb.getDeviceAddress(device);
}
}
}
finally
{
LibUsb.freeDeviceList(deviceList, true);
}
}
finally
{
LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
}
@Override

View File

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

View File

@@ -20,7 +20,7 @@ package io.github.dsheirer.source.tuner.fcd;
public enum FCDCommand
{
/**
* Note: the libusb interrupt transfers used here (through jlibusb) are slightly different than
* Note: the javaxUSB and usb4Java libraries 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.

View File

@@ -19,11 +19,6 @@
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;
@@ -33,13 +28,21 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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
@@ -47,19 +50,16 @@ 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 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 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 int mBus;
private String mPortAddress;
private UsbContext mUsbContext;
private UsbDeviceInfo mDevice;
private volatile UsbDeviceHandle mDeviceHandle;
private boolean mShutdown = false;
private Context mDeviceContext = new Context();
private Device mDevice;
private DeviceDescriptor mDeviceDescriptor = new DeviceDescriptor();
private DeviceHandle mDeviceHandle = new DeviceHandle();
private FCDConfiguration mConfiguration = new FCDConfiguration();
protected ComplexMixer mComplexMixer;
@@ -168,6 +168,58 @@ 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.
@@ -176,102 +228,80 @@ public abstract class FCDTunerController extends TunerController
*/
public void start() throws SourceException
{
if(mShutdown)
if(mDeviceContext == null)
{
throw new SourceException("Device cannot be reused once it has been shutdown");
}
mUsbContext = UsbDeviceLookup.createContext();
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));
}
try
{
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()));
}
deviceStart();
}
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;
}
}
/**
@@ -291,9 +321,23 @@ public abstract class FCDTunerController extends TunerController
mComplexMixer = null;
}
closeUsb();
if(mDeviceHandle != null)
{
try
{
LibUsb.close(mDeviceHandle);
}
catch(Exception e)
{
mLog.error("error while closing device handle", e);
}
}
mDeviceDescriptor = null;
mDeviceHandle = null;
mDevice = null;
mShutdown = true;
LibUsb.exit(mDeviceContext);
mDeviceContext = null;
}
/**
@@ -316,14 +360,19 @@ public abstract class FCDTunerController extends TunerController
*/
public String getUSBAddress()
{
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)
if(mDevice != null)
{
int port = device.portNumbers().isEmpty() ? 0 : device.portNumbers().getLast();
return "Bus:" + device.busNumber() + " Port:" + port;
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();
}
return "UNKNOWN";
@@ -334,34 +383,38 @@ public abstract class FCDTunerController extends TunerController
*/
public String getUSBID()
{
UsbDeviceInfo device = mDevice;
if(device != null)
if(mDeviceDescriptor != null)
{
return String.format("%04X:%04X", device.vendorId(), device.productId());
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 "UNKNOWN";
}
/**
* USB Port Speed. The FUNcube Dongle Pro is a full speed (USB 1.1) device.
* USB Port Speed. Should be 2.0 for both types of funcube dongles
*/
public String getUSBSpeed()
{
UsbDeviceInfo device = mDevice;
if(device != null)
if(mDevice != null)
{
switch(device.speed())
int speed = LibUsb.getDeviceSpeed(mDevice);
switch(speed)
{
case LOW:
case 0:
return "1.1 LOW";
case FULL:
case 1:
return "1.1 FULL";
case HIGH:
case 2:
return "2.0 HIGH";
case SUPER:
case 3:
return "3.0 SUPER";
default:
}
@@ -447,27 +500,26 @@ public abstract class FCDTunerController extends TunerController
}
/**
* Performs an interrupt write of one 64 byte packet to the OUT endpoint.
* Performs an interrupt write to the OUT endpoint.
*
* @param packet to write. Must be 64 bytes in length.
* @throws FCDUsbException on error
* @param buffer - direct allocated buffer. Must be 64 bytes in length.
* @throws LibUsbException on error
*/
private void write(byte[] packet) throws FCDUsbException
private void write(ByteBuffer buffer) throws LibUsbException
{
UsbDeviceHandle handle = mDeviceHandle;
if(mDeviceHandle != null)
{
IntBuffer transferred = IntBuffer.allocate(1);
int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_OUT, buffer, transferred, 500l);
if(handle == null)
{
throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
if(result != LibUsb.SUCCESS)
{
throw new LibUsbException("error writing byte buffer", result);
}
}
try
else
{
handle.interruptTransfer(FCD_ENDPOINT_OUT, packet, packet.length, FCD_TRANSFER_TIMEOUT_MS);
}
catch(UsbException e)
{
throw new FCDUsbException("error writing byte buffer", e.code());
throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE);
}
}
@@ -475,14 +527,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 FCDUsbException - on error
* @throws LibUsbException - on error
*/
private void write(FCDCommand command) throws FCDUsbException
private void write(FCDCommand command) throws LibUsbException
{
byte[] packet = new byte[FCD_PACKET_SIZE];
packet[0] = command.getCommand();
packet[1] = (byte) 0x00;
write(packet);
ByteBuffer buffer = ByteBuffer.allocateDirect(64);
buffer.put(0, command.getCommand());
buffer.put(1, (byte) 0x00);
write(buffer);
}
/**
@@ -490,53 +542,46 @@ public abstract class FCDTunerController extends TunerController
*
* @param command - command to write
* @param argument - value to write with the command
* @throws FCDUsbException - on error
* @throws LibUsbException - on error
*/
private void write(FCDCommand command, long argument) throws FCDUsbException
private void write(FCDCommand command, long argument) throws LibUsbException
{
byte[] packet = new byte[FCD_PACKET_SIZE];
ByteBuffer buffer = ByteBuffer.wrap(packet);
ByteBuffer buffer = ByteBuffer.allocateDirect(64);
/* The FCD expects little-endian formatted values */
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(0, command.getCommand());
buffer.putLong(1, argument);
write(packet);
write(buffer);
}
/**
* Performs an interrupt read of one 64 byte packet from the IN endpoint
* Performs an interrupt read against the endpoint
*
* @return buffer read from FCD
* @throws FCDUsbException on error
* @throws LibUsbException on error
*/
private ByteBuffer read() throws FCDUsbException
private ByteBuffer read() throws LibUsbException
{
UsbDeviceHandle handle = mDeviceHandle;
if(handle == null)
if(mDeviceHandle != null)
{
throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
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;
}
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);
throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE);
}
/**
@@ -545,9 +590,9 @@ public abstract class FCDTunerController extends TunerController
*
* @param command - command to send
* @param argument - command argument to send
* @throws FCDUsbException - on error
* @throws LibUsbException - on error
*/
protected synchronized void send(FCDCommand command, long argument) throws FCDUsbException
protected synchronized void send(FCDCommand command, long argument) throws LibUsbException
{
write(command, argument);
read();
@@ -558,9 +603,9 @@ public abstract class FCDTunerController extends TunerController
* command.
*
* @param command - command to send
* @throws FCDUsbException - on error
* @throws LibUsbException - on error
*/
protected ByteBuffer send(FCDCommand command) throws FCDUsbException
protected ByteBuffer send(FCDCommand command) throws LibUsbException
{
write(command);
return read();

View File

@@ -1,50 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.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;
}
}

View File

@@ -32,11 +32,17 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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).
@@ -246,21 +252,110 @@ 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.
* specific device by serial number. The device is closed again before returning so that libhackrf can open it.
* @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
{
String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
Context context = new Context();
int status = LibUsb.init(context);
//libhackrf compares, at most, the last 32 characters of the serial string
if(serial != null && serial.length() > 32)
if(status != LibUsb.SUCCESS)
{
serial = serial.substring(serial.length() - 32);
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
return serial == null || serial.isEmpty() ? null : serial;
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);
}
}
/**

View File

@@ -34,13 +34,19 @@ 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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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).
@@ -203,17 +209,98 @@ 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 USB serial string is formatted as: HYDRASDR SN:0123456789ABCDEF
* device by serial number. The device is closed again before returning so that libhydrasdr can open it.
* @return serial number
* @throws SourceException if the device isn't found or can't be accessed.
*/
private long readSerialNumber() throws SourceException
{
String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
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
{
return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1).trim(), 16);
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);
}
catch(NullPointerException | NumberFormatException e)
{

View File

@@ -1,35 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
/**
* LimeSDR device details for display.
* @param serial board serial number, 16 hex digits
* @param board name, such as LimeSDR-USB or LimeSDR-Mini
* @param firmware version
* @param hardware version
* @param gateware (FPGA) version
* @param connection USB link, such as USB3.0
* @param library version of LimeSuiteNG
*/
public record LimeSdrDeviceInformation(String serial, String board, String firmware, String hardware, String gateware,
String connection, String library)
{
}

View File

@@ -1,112 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import java.util.ArrayList;
import java.util.List;
/**
* LimeSDR sample rates offered to the user. The LMS7002M supports any rate in its range, so these are rates that divide
* evenly into polyphase channelizer channels.
*/
public enum LimeSdrSampleRate
{
RATE_1_25(1_250_000, "1.25 MHz"),
RATE_2_5(2_500_000, "2.50 MHz"),
RATE_5_0(5_000_000, "5.00 MHz"),
RATE_7_5(7_500_000, "7.50 MHz"),
RATE_10_0(10_000_000, "10.00 MHz"),
RATE_12_5(12_500_000, "12.50 MHz"),
RATE_15_0(15_000_000, "15.00 MHz"),
RATE_20_0(20_000_000, "20.00 MHz"),
RATE_25_0(25_000_000, "25.00 MHz"),
RATE_30_0(30_000_000, "30.00 MHz"),
RATE_30_72(30_720_000, "30.72 MHz"),
RATE_40_0(40_000_000, "40.00 MHz"),
RATE_50_0(50_000_000, "50.00 MHz"),
RATE_60_0(60_000_000, "60.00 MHz"),
RATE_61_44(61_440_000, "61.44 MHz");
public static final LimeSdrSampleRate DEFAULT = RATE_10_0;
private final int mRate;
private final String mLabel;
LimeSdrSampleRate(int rate, String label)
{
mRate = rate;
mLabel = label;
}
/**
* Sample rate in Hertz
*/
public int getRate()
{
return mRate;
}
@Override
public String toString()
{
return mLabel;
}
/**
* Sample rates within the device range, which is the lower of what the board and its USB connection support.
* @param minimumRate in Hertz
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/
public static List<LimeSdrSampleRate> getRates(double minimumRate, double maximumRate)
{
List<LimeSdrSampleRate> rates = new ArrayList<>();
for(LimeSdrSampleRate rate : values())
{
if(rate.getRate() >= minimumRate && rate.getRate() <= maximumRate)
{
rates.add(rate);
}
}
return rates;
}
/**
* Finds the highest rate from the list that doesn't exceed the requested rate.
* @param rates to search, lowest to highest
* @param requested rate in Hertz
* @return closest rate, or the lowest rate when the requested rate is below all of them.
*/
public static LimeSdrSampleRate getClosest(List<LimeSdrSampleRate> rates, int requested)
{
LimeSdrSampleRate closest = rates.getFirst();
for(LimeSdrSampleRate rate : rates)
{
if(rate.getRate() <= requested)
{
closest = rate;
}
}
return closest;
}
}

View File

@@ -1,86 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import io.github.dsheirer.preference.source.ChannelizerType;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerClass;
/**
* LimeSDR tuner
*/
public class LimeSdrTuner extends Tuner
{
//The USB link carries 12-bit I and Q samples, packed into 3 bytes
private static final int LINK_BITS_PER_SAMPLE = 24;
private static final double SAMPLE_SIZE_12_BIT = 12.0;
/**
* Constructs an instance
* @param controller for the LimeSDR
* @param tunerErrorListener to listen for errors from this tuner
* @param channelizerType for the channelizer
*/
public LimeSdrTuner(LimeSdrTunerController controller, ITunerErrorListener tunerErrorListener,
ChannelizerType channelizerType)
{
super(controller, tunerErrorListener, channelizerType);
}
/**
* LimeSDR tuner controller
*/
public LimeSdrTunerController getController()
{
return (LimeSdrTunerController)getTunerController();
}
@Override
public String getPreferredName()
{
LimeSdrDeviceInformation info = getController().getDeviceInfo();
return (info != null ? info.board() : "LimeSDR") + " " + getUniqueID();
}
@Override
public String getUniqueID()
{
return getController().getDeviceInfo().serial();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.LIMESDR;
}
@Override
public double getSampleSize()
{
return SAMPLE_SIZE_12_BIT;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return LimeSdrSampleRate.RATE_61_44.getRate() * LINK_BITS_PER_SAMPLE;
}
}

View File

@@ -1,97 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
/**
* LimeSDR tuner configuration
*/
public class LimeSdrTunerConfiguration extends TunerConfiguration
{
private int mSampleRate = LimeSdrSampleRate.DEFAULT.getRate();
private int mGain = LimeSdrTunerController.GAIN_DEFAULT;
private String mAntenna = LimeSdrTunerController.ANTENNA_DEFAULT;
/**
* Default constructor for Jackson
*/
public LimeSdrTunerConfiguration()
{
super(LimeSdrTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, LimeSdrTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ);
}
/**
* Constructs an instance
* @param uniqueID for the tuner
*/
public LimeSdrTunerConfiguration(String uniqueID)
{
super(uniqueID);
}
@Override
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public TunerType getTunerType()
{
return TunerType.LIMESDR;
}
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
/**
* Combined receive gain in dB, 0 to 73.
*/
@JacksonXmlProperty(isAttribute = true, localName = "gain")
public int getGain()
{
return mGain;
}
public void setGain(int gain)
{
mGain = gain;
}
/**
* Receive antenna port name, such as LNAW.
*/
@JacksonXmlProperty(isAttribute = true, localName = "antenna")
public String getAntenna()
{
return mAntenna;
}
public void setAntenna(String antenna)
{
mAntenna = antenna;
}
}

View File

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

View File

@@ -20,13 +20,6 @@
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;
@@ -52,13 +45,22 @@ 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
@@ -71,12 +73,9 @@ 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 boolean mLibUsbAvailable = false;
private final Context mLibUsbApplicationContext = new Context();
private boolean mLibUsbInitialized = false;
private SDRplay mSDRplay;
/**
@@ -113,26 +112,60 @@ public class TunerManager implements IDiscoveredTunerStatusListener
{
mLog.info("Discovering tuners ...");
boolean libUsbAvailable = false;
try
{
//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;
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());
}
catch(LinkageError | RuntimeException e)
catch(Exception 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=<directory containing libusb>", 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);
}
}
if(mLibUsbAvailable)
if(libUsbAvailable)
{
discoverUSBTuners();
mHotplugEventSupport.start();
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();
}
discoverSdrPlayTuners();
}
@@ -144,14 +177,19 @@ 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;
}
}
/**
@@ -167,125 +205,103 @@ public class TunerManager implements IDiscoveredTunerStatusListener
*/
private void discoverUSBTuners()
{
List<DiscoveredUSBTuner> discoveredUSBTuners = new ArrayList<>();
if(mLibUsbInitialized)
{
List<DiscoveredUSBTuner> discoveredUSBTuners = new ArrayList<>();
try(UsbContext usb = UsbContext.create())
{
List<UsbDeviceInfo> devices = getDeviceList(usb);
if(devices != null)
{
mLog.info("LibUsb - discovered [" + devices.size() + "] potential usb devices");
for(UsbDeviceInfo device: devices)
{
DiscoveredUSBTuner discoveredUSBTuner = createDiscoveredTuner(device);
if(discoveredUSBTuner != null)
{
mLog.info("Discovered tuner at USB Bus [" + discoveredUSBTuner.getBus() + "] Port [" +
discoveredUSBTuner.getPortAddress() + "] Tuner Class [" +
discoveredUSBTuner.getTunerClass() + "]");
discoveredUSBTuners.add(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<UsbDeviceInfo> 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<UsbDeviceInfo> 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);
DeviceList deviceList = new DeviceList();
int deviceCount = LibUsb.getDeviceList(mLibUsbApplicationContext, deviceList);
if(deviceCount >= 0)
{
mLog.info("LibUsb - discovered [" + deviceCount + "] potential usb devices");
for(Device device: deviceList)
{
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);
}
}
else
{
mLog.error("LibUsb - error [" + deviceCount + "/" + LibUsb.errorName(deviceCount) + "] during get device list");
}
}
catch(UsbException e)
catch(Exception e)
{
//Logging level only - nothing else to do
mLog.error("LibUsb - error during USB device discovery", e);
}
mLog.info("LibUsb - get device list reattempt complete - if additional logging was generated please notify developer");
for(DiscoveredUSBTuner discoveredUSBTuner: discoveredUSBTuners)
{
addUsbTuner(discoveredUSBTuner);
}
}
}
/**
* 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);
}
/**
* Determines if the USB device is a supported tuner and add if it has not already been added/discovered.
* @param discoveredUSBTuner to add
@@ -664,104 +680,161 @@ public class TunerManager implements IDiscoveredTunerStatusListener
}
/**
* 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).
*
* 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.
* 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 class HotplugEventSupport implements UsbHotplugListener
public static String getPortAddress(Device device)
{
private UsbHotplug mUsbHotplug;
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.
*
* Note: hotplug is not supported on all platforms.
*/
public class HotplugEventSupport implements HotplugCallback
{
private static final int HOTPLUG_CONTINUE_EVENT_SUPPORT = 0;
private HotplugCallbackHandle mHotplugCallbackHandle;
private ScheduledFuture<?> mEventProcessorFuture;
/**
* Starts hotplug detection of supported USB tuners.
* 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.
*/
public void start()
{
try
if(LibUsb.hasCapability(LibUsb.CAP_HAS_HOTPLUG))
{
mUsbHotplug = UsbHotplug.start(device -> getSupportedTunerClass(device) != null, this);
mLog.info("LibUsb - started USB hotplug detection (" +
(mUsbHotplug.mode() == UsbHotplug.Mode.NATIVE ? "native" : "polling") + ")");
}
catch(UsbException | RuntimeException e)
{
mLog.error("LibUsb - unable to start USB hotplug detection", e);
mUsbHotplug = null;
}
}
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;
/**
* Stops hotplug detection. Waits for an arrival or removal that is being processed to finish.
*/
public void stop()
{
if(mUsbHotplug != null)
{
mUsbHotplug.close();
mUsbHotplug = null;
}
}
mLog.info("LibUsb - starting HotPlug detection");
int status = LibUsb.hotplugRegisterCallback(mLibUsbApplicationContext, events, flags, vendorId,
productId, deviceClass, this, "sdrtrunk hotplug support", mHotplugCallbackHandle);
/**
* A supported USB tuner was connected. Called on the jlibusb hotplug dispatch thread.
* @param device that arrived
*/
@Override
public void arrived(UsbDeviceInfo device)
{
try
{
DiscoveredUSBTuner discoveredUSBTuner = createDiscoveredTuner(device);
if(discoveredUSBTuner == null ||
mDiscoveredTunerModel.hasUsbTuner(discoveredUSBTuner.getBus(), discoveredUSBTuner.getPortAddress()))
if(status == LibUsb.SUCCESS)
{
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);
Runnable eventHandler = () -> LibUsb.handleEvents(mLibUsbApplicationContext);
mEventProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(eventHandler,
0, 1, TimeUnit.SECONDS);
}
else
{
addUsbTuner(discoveredUSBTuner);
mHotplugCallbackHandle = null;
mLog.info("LibUsb - unable to register device hotplug listener - " + LibUsb.errorName(status));
}
}
catch(Exception e)
else
{
mLog.error("Error adding USB tuner after plug-in at USB Bus [" + device.busNumber() + "] Port [" +
UsbPortAddress.of(device) + "]", e);
mLog.info("LibUsb Hotplug event notification Is Not Supported on this platform.");
}
}
/**
* A supported USB tuner was disconnected. Called on the jlibusb hotplug dispatch thread.
* @param device that left
* Unregisters from hotplug event notifications and stops the event processing timer
*/
@Override
public void left(UsbDeviceInfo device)
public void stop()
{
try
if(mHotplugCallbackHandle != null)
{
DiscoveredTuner removed = removeUsbTuner(device.busNumber(), UsbPortAddress.of(device));
if(removed != null)
{
mLog.info("Tuner Unplugged: " + removed.getId());
}
LibUsb.hotplugDeregisterCallback(mLibUsbApplicationContext, mHotplugCallbackHandle);
mHotplugCallbackHandle = null;
}
catch(Exception e)
if(mEventProcessorFuture != null)
{
mLog.error("Error removing USB tuner after unplug at USB Bus [" + device.busNumber() + "] Port [" +
UsbPortAddress.of(device) + "]", e);
mEventProcessorFuture.cancel(true);
mEventProcessorFuture = null;
}
}
}

View File

@@ -1,195 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.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<UsbDeviceInfo> 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));
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.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<Integer> ports)
{
return ports.stream().map(String::valueOf).collect(Collectors.joining("."));
}
}

View File

@@ -18,10 +18,6 @@
*/
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;
@@ -36,7 +32,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.UsbDeviceLookup;
import io.github.dsheirer.source.tuner.manager.TunerManager;
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;
@@ -48,6 +44,12 @@ 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).
@@ -326,40 +328,73 @@ public class RTL2832TunerController extends TunerController
*/
private UsbIdentity readUsbIdentity() throws SourceException
{
try(UsbContext usb = UsbDeviceLookup.createContext())
Context context = new Context();
int status = LibUsb.init(context);
if(status != LibUsb.SUCCESS)
{
//RTL-2832 devices that precede this controller's device in the device list
List<UsbDeviceInfo> earlierDevices = new ArrayList<>();
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
for(UsbDeviceInfo device : UsbDeviceLookup.devices(usb))
try
{
DeviceList deviceList = new DeviceList();
int count = LibUsb.getDeviceList(context, deviceList);
if(count < 0)
{
if(TunerClass.lookup((short)device.vendorId(), (short)device.productId()) != TunerClass.RTL2832)
{
continue;
}
throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
}
if(UsbDeviceLookup.isAt(device, mBus, mPortAddress))
{
mUsbSerialNumberIndex = device.serialNumberIndex();
String serial = readSerial(usb, device, true);
int serialPosition = 0;
try
{
//RTL-2832 devices that precede this controller's device in the device list
List<Device> earlierDevices = new ArrayList<>();
List<DeviceDescriptor> earlierDescriptors = new ArrayList<>();
if(serial != null)
for(Device device : deviceList)
{
DeviceDescriptor descriptor = new DeviceDescriptor();
if(LibUsb.getDeviceDescriptor(device, descriptor) != LibUsb.SUCCESS ||
TunerClass.lookup(descriptor.idVendor(), descriptor.idProduct()) != TunerClass.RTL2832)
{
for(UsbDeviceInfo earlierDevice : earlierDevices)
{
if(serial.equals(readSerial(usb, earlierDevice, false)))
{
serialPosition++;
}
}
continue;
}
return new UsbIdentity(serial, serialPosition, earlierDevices.size());
}
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;
earlierDevices.add(device);
if(serial != null)
{
for(int x = 0; x < earlierDevices.size(); x++)
{
if(serial.equals(readSerial(earlierDevices.get(x), earlierDescriptors.get(x), false)))
{
serialPosition++;
}
}
}
return new UsbIdentity(serial, serialPosition, earlierDevices.size());
}
earlierDevices.add(device);
earlierDescriptors.add(descriptor);
}
}
finally
{
LibUsb.freeDeviceList(deviceList, true);
}
}
finally
{
LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
@@ -367,32 +402,51 @@ 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(UsbContext usb, UsbDeviceInfo device, boolean required) throws SourceException
private static String readSerial(Device device, DeviceDescriptor descriptor, boolean required) throws SourceException
{
if(!required)
if(descriptor.iSerialNumber() == 0)
{
if(device.serialNumberIndex() == 0)
{
return null;
}
try(UsbDeviceHandle handle = usb.open(device))
{
return handle.readString(device.serialNumberIndex());
}
catch(UsbException e)
{
return null;
}
return null;
}
return UsbDeviceLookup.readSerialNumber(usb, device);
DeviceHandle handle = new DeviceHandle();
int status = LibUsb.open(device, handle);
if(status != LibUsb.SUCCESS)
{
if(!required)
{
return null;
}
if(status == LibUsb.ERROR_ACCESS)
{
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");
}
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);
}
}
/**

View File

@@ -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().orElse(null);
usbTuner.isAt(bus, portAddress)).findFirst().get();
if(discoveredTuner != null)
{

View File

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

View File

@@ -1,178 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.voice;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for AMBE voice frame error detection.
*/
public class AMBEFrameValidatorTest
{
//Base station voice frame A burst from a recording where JMBE decodes all three voice frames with zero errors.
private static final String VOICE_BURST = "5A3AC4B9E881526173002A6BB9E881526755FD7DF75F7173002A6BB9E881526173002A6B";
//Burst bit indices of the first C0 bits (voice frame bits 0, 4, 8) and first C1 bit (voice frame bit 25) of
//frame 1, which starts at burst bit 24.
private static final int FRAME_1_C0_BIT_0 = 24;
private static final int FRAME_1_C0_BIT_1 = 28;
private static final int FRAME_1_C0_BIT_2 = 32;
private static final int FRAME_1_C1_BIT_0 = 49;
private static CorrectedBinaryMessage getBurst()
{
return CorrectedBinaryMessage.loadHex(VOICE_BURST);
}
@Test
void errorFreeBurst()
{
CorrectedBinaryMessage burst = getBurst();
for(int frame = 0; frame < 3; frame++)
{
assertEquals(0, AMBEFrameValidator.getErrorCount(burst, frame), "Frame " + frame);
}
assertTrue(AMBEFrameValidator.isValid(burst));
}
@Test
void errorsAreCounted()
{
CorrectedBinaryMessage burst = getBurst();
burst.flip(FRAME_1_C0_BIT_0);
assertEquals(1, AMBEFrameValidator.getErrorCount(burst, 0));
burst.flip(FRAME_1_C1_BIT_0);
assertEquals(2, AMBEFrameValidator.getErrorCount(burst, 0));
burst.flip(FRAME_1_C0_BIT_1);
assertEquals(3, AMBEFrameValidator.getErrorCount(burst, 0));
}
@Test
void fourC0ErrorsAreDetected()
{
CorrectedBinaryMessage burst = getBurst();
burst.flip(FRAME_1_C0_BIT_0);
burst.flip(FRAME_1_C0_BIT_1);
burst.flip(FRAME_1_C0_BIT_2);
burst.flip(FRAME_1_C0_BIT_2 + 4);
assertTrue(AMBEFrameValidator.getErrorCount(burst, 0) > AMBEFrameValidator.MAX_ERRORS);
}
@Test
void validWhenAnyFrameIsValid()
{
CorrectedBinaryMessage burst = getBurst();
//Corrupt the C0 vector of each frame in turn (4 bit errors), verifying a single valid frame is sufficient.
int[] frameOffsets = {24, 96, 216};
for(int frame = 0; frame < 3; frame++)
{
for(int bit = 0; bit < 16; bit += 4)
{
burst.flip(frameOffsets[frame] + bit);
}
assertTrue(AMBEFrameValidator.getErrorCount(burst, frame) > AMBEFrameValidator.MAX_ERRORS);
assertEquals(frame < 2, AMBEFrameValidator.isValid(burst), "Corrupted frames: " + (frame + 1));
}
}
@Test
void randomBurstsAreRarelyValid()
{
Random random = new Random(1234);
int valid = 0;
int trials = 20000;
for(int trial = 0; trial < trials; trial++)
{
CorrectedBinaryMessage burst = new CorrectedBinaryMessage(288);
for(int bit = 0; bit < 288; bit++)
{
if(random.nextBoolean())
{
burst.set(bit);
}
}
if(AMBEFrameValidator.isValid(burst))
{
valid++;
}
}
//Expected false valid rate is ~3 x 1.3e-4 (ie ~8 of 20,000)
assertTrue(valid < 30, "Random bursts detected as valid: " + valid);
}
@Test
void voiceMessageValidity()
{
assertTrue(new VoiceAMessage(DMRSyncPattern.BASE_STATION_VOICE, getBurst(), null, 0, 1).isValid());
CorrectedBinaryMessage corrupted = getBurst();
int[] frameOffsets = {24, 96, 216};
for(int offset : frameOffsets)
{
for(int bit = 0; bit < 16; bit += 4)
{
corrupted.flip(offset + bit);
}
}
assertFalse(new VoiceAMessage(DMRSyncPattern.BASE_STATION_VOICE, corrupted, null, 0, 1).isValid());
}
/**
* Verifies the AMBE frames match the burst bits, specifically for frame 2 where bits 32-35 are sourced from burst
* bits 128-131 and bits 36-39 from burst bits 180-183, with burst bit 180 set (ie a negative signed byte).
*/
@Test
void ambeFramesMatchBurstBits()
{
CorrectedBinaryMessage burst = getBurst();
burst.set(180);
burst.clear(128, 132);
List<byte[]> frames = new VoiceAMessage(DMRSyncPattern.BASE_STATION_VOICE, burst, null, 0, 1).getAMBEFrames();
int[] frameOffsets = {24, 96, 216};
for(int frame = 0; frame < 3; frame++)
{
for(int bit = 0; bit < 72; bit++)
{
int burstIndex = frameOffsets[frame] + bit + ((frame == 1 && bit >= 36) ? 48 : 0);
boolean frameBit = (frames.get(frame)[bit / 8] & (0x80 >> (bit % 8))) != 0;
assertEquals(burst.get(burstIndex), frameBit, "Frame " + frame + " bit " + bit);
}
}
}
}

View File

@@ -1,204 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.dsp.filter.channelizer.ComplexPolyphaseChannelizerM2;
import io.github.dsheirer.limesdr.DeviceEntry;
import io.github.dsheirer.limesdr.LogLevel;
import io.github.dsheirer.limesdr.Range;
import io.github.dsheirer.limesdr.StreamConfig;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerFactory;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Tests for the parts of the LimeSDR tuner that don't require a device.
*/
class LimeSdrTunerTest
{
private static final DeviceEntry USB_A = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00462008");
private static final DeviceEntry USB_B = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00471B22");
private static final DeviceEntry MINI = DeviceEntry.parse("LimeSDR-Mini, media=USB3.0, addr=0403:601f, serial=1D3AC9A5C7B4D2");
@Test
void lookupFindsEachUsbProduct()
{
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x1D50, (short)0x6108));
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x0403, (short)0x601F));
assertTrue(TunerClass.LIMESDR.isSupportedUsbTuner());
assertTrue(TunerType.LIMESDR.isSupportedUsbTuner());
}
@Test
void scalesSamplesAndRemovesHalfBitOffset()
{
short[] samples = {2047, -2048, -1, 0};
assertArrayEquals(new float[]{2047.5f / 2048, -2047.5f / 2048, -0.5f / 2048, 0.5f / 2048},
LimeSdrTunerController.scale(samples));
}
@Test
void sampleRatesAreLimitedByTheDevice()
{
//LimeSDR-USB on USB 3.0
assertEquals(List.of(LimeSdrSampleRate.values()), LimeSdrSampleRate.getRates(100_000, 61_440_000));
//LimeSDR Mini
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getRates(100_000, 30_720_000).getLast());
}
@Test
void usb2LimitsTheSampleRate()
{
Range range = new Range(100_000, 61_440_000, 0);
assertEquals(61_440_000, LimeSdrTunerController.getMaximumSampleRate(range, "USB3.0"));
double usb2 = LimeSdrTunerController.getMaximumSampleRate(range, "USB2.0");
assertEquals(LimeSdrSampleRate.RATE_10_0, LimeSdrSampleRate.getRates(range.min(), usb2).getLast());
}
@Test
void savedRateAboveTheDeviceMaximumIsReduced()
{
List<LimeSdrSampleRate> rates = LimeSdrSampleRate.getRates(100_000, 30_720_000);
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getClosest(rates, LimeSdrSampleRate.RATE_61_44.getRate()));
assertEquals(LimeSdrSampleRate.RATE_1_25, LimeSdrSampleRate.getClosest(rates, 1));
}
@Test
void sampleRatesDivideIntoPolyphaseChannels()
{
for(LimeSdrSampleRate rate : LimeSdrSampleRate.values())
{
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(rate.getRate());
assertEquals(0, channelCount % 2, rate.toString());
assertTrue((double)rate.getRate() / channelCount >= 25_000.0, rate.toString());
}
}
@Test
void maximumUsbRateDoesNotOverflow()
{
assertTrue(LimeSdrSampleRate.RATE_61_44.getRate() * 24L <= Integer.MAX_VALUE);
}
@Test
void antennaListExcludesNoneAndLoopbackPorts()
{
List<String> ports = List.of("None", "LNAH", "LNAL", "LNAW", "LB1", "LB2");
List<String> antennas = LimeSdrTunerController.getSelectableAntennas(ports);
assertEquals(List.of("LNAH", "LNAL", "LNAW"), antennas);
assertEquals("LNAW", LimeSdrTunerController.getDefaultAntenna(antennas));
assertEquals("LNAH", LimeSdrTunerController.getDefaultAntenna(List.of("LNAH")));
}
@Test
void deviceIsFoundBySerialNumber() throws SourceException
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00471B22");
assertEquals(USB_B, LimeSdrTunerController.findDevice(identity, List.of(MINI, USB_A, USB_B)));
}
@Test
void deviceWithoutSerialIsFoundWhenItIsTheOnlyOneOfItsKind() throws SourceException
{
LimeSdrTunerController.UsbIdentity mini = new LimeSdrTunerController.UsbIdentity(0x0403, 0x601F, null);
LimeSdrTunerController.UsbIdentity usb = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108, null);
assertEquals(MINI, LimeSdrTunerController.findDevice(mini, List.of(USB_A, MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(usb, List.of(USB_A, USB_B, MINI)));
}
@Test
void deviceThatLimeSuiteCantSeeIsAnError()
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00462008");
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(USB_B)));
}
@Test
void configurationSurvivesSerialization() throws Exception
{
LimeSdrTunerConfiguration configuration = new LimeSdrTunerConfiguration("test");
configuration.setSampleRate(LimeSdrSampleRate.RATE_5_0.getRate());
configuration.setGain(55);
configuration.setAntenna("LNAH");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = mapper.writeValueAsString(configuration);
TunerConfiguration restored = mapper.readValue(json, TunerConfiguration.class);
LimeSdrTunerConfiguration limeSdr = assertInstanceOf(LimeSdrTunerConfiguration.class, restored);
assertEquals(TunerType.LIMESDR, limeSdr.getTunerType());
assertEquals(LimeSdrSampleRate.RATE_5_0.getRate(), limeSdr.getSampleRate());
assertEquals(55, limeSdr.getGain());
assertEquals("LNAH", limeSdr.getAntenna());
assertEquals("test", limeSdr.getUniqueID());
}
@Test
void factoryCreatesTheConfiguration()
{
assertInstanceOf(LimeSdrTunerConfiguration.class, TunerFactory.getTunerConfiguration(TunerType.LIMESDR, "id"));
}
@Test
void fifoHoldsAQuarterSecondOfSamples()
{
assertEquals(StreamConfig.DEFAULT_FIFO_SIZE, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_1_25));
assertEquals(10_000_000, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_40_0));
}
@Test
void debugMessagesAreDiscarded()
{
LimeSdrTunerController.log(LogLevel.DEBUG, "Loss: pkt:4622555 exp: 000000017a863dc0, got: 000000017a864860");
LimeSdrTunerController.log(LogLevel.VERBOSE, "Set Rx LO frequency (450100180 Hz)");
}
@Test
void droppedSamplesAreFoundFromTimestampGaps()
{
assertEquals(0, LimeSdrTunerController.getTimestampGap(-1, 5_000_000));
assertEquals(0, LimeSdrTunerController.getTimestampGap(65536, 65536));
assertEquals(718_080, LimeSdrTunerController.getTimestampGap(32_309_248, 33_027_328));
assertEquals(0, LimeSdrTunerController.getTimestampGap(32_309_248, 0));
}
}