Compare commits

..

1 Commits

Author SHA1 Message Date
denny
c05fd33ca6 #697 HackRF Bias T control. 2026-11-07 07:07:22 -05:00
123 changed files with 5007 additions and 5681 deletions

View File

@@ -14,9 +14,6 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
# Full history and tags, so the build can describe the commit relative to the latest version tag
fetch-depth: 0
- name: Set up JDK 27
uses: actions/setup-java@v6
@@ -37,7 +34,7 @@ jobs:
- name: Create Build Info file
run: |
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\ndescribe: $(git describe --tags --match 'v[0-9]*' --dirty)\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
- name: Add build artifacts to nightly release
uses: softprops/action-gh-release@v3

View File

@@ -1,6 +1,6 @@
# Publishes a release when a version tag is pushed: v0.6.2 for a final release,
# or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release. The build derives the
# version from the tag (see build.gradle).
# Publishes a release when a version tag is pushed. The tag has to match
# projectVersion in gradle.properties with a leading 'v': v0.6.2 for a final
# release, or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release.
name: Release
on:
@@ -22,6 +22,15 @@ jobs:
# Full history and tags, to find the previous release tag for the release notes
fetch-depth: 0
- id: version
run: |
expected="v$(sed -n 's/^projectVersion=//p' gradle.properties | tr -d '[:space:]')"
if [ "$GITHUB_REF_NAME" != "$expected" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match projectVersion in gradle.properties ($expected)"
exit 1
fi
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# Release notes list the PRs since the previous version tag (never 'nightly'). A final release
# starts from the previous final release, so it covers every alpha and beta in between.
- id: previous
@@ -45,17 +54,6 @@ jobs:
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
# The version comes from 'git describe', which can pick a different tag when more than one
# version tag points at the same commit. Fail rather than publish zips with the wrong version.
- id: version
run: |
version="$(./gradlew -q printVersion --console=plain | tail -n 1)"
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::Build version $version does not match tag $GITHUB_REF_NAME"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Build and test
run: ./gradlew clean build --console=plain

View File

@@ -54,19 +54,17 @@ committed to the code base, so it's not really 'nightly' as much as it is 'curre
Releases are built and published automatically by the [Release](.github/workflows/release.yml) GitHub workflow
whenever a version tag is pushed to the repository.
The version is taken from the tag, so there is no version number to update in the code. Create and push a tag
with a leading `v`, following the naming convention described in `gradle.properties` (e.g. `v0.6.2-alpha-1`,
`v0.6.2-beta-1` or `v0.6.2`):
1. Update `projectVersion` in `gradle.properties` to the new version, following the naming convention described in
that file (e.g. `0.6.2-alpha-1`, `0.6.2-beta-1` or `0.6.2`).
2. Commit and push the change to `master`.
3. Create and push a tag that matches the version with a leading `v`:
```
git tag v0.6.2-beta-1
git push origin v0.6.2-beta-1
```
Builds made from an untagged commit get a version like `0.6.2-beta-1-14-g4626d567` (14 commits after the
`v0.6.2-beta-1` tag, at commit `4626d567`), with `-dirty` appended when there are uncommitted changes.
The workflow then:
* Verifies that the version the build derives from git matches the tag and fails if it does not.
* Verifies that the tag matches `projectVersion` in `gradle.properties` and fails if it does not.
* Builds and tests the code, then creates the release zips for Linux, Mac and Windows (x86-64 and aarch64).
* Creates a GitHub release titled `Version <version>` with auto-generated release notes (a "What's Changed" list of
the PRs merged since the previous version tag), and attaches the six zip

View File

@@ -52,44 +52,8 @@ plugins {
id 'org.beryx.runtime' version '2.0.1'
}
/**
* Version is derived from the most recent 'v<version>' git tag (e.g. v1.0.0-beta-1 -> 1.0.0-beta-1). Commits after
* the tag produce a version like 1.0.0-beta-1-14-g4626d567 and uncommitted changes append '-dirty'. The moving
* 'nightly' tag is ignored. Override with -PprojectVersion=<version> (the nightly build uses 'nightly' so that the
* nightly release asset names stay the same). Falls back to 'unknown' when built outside a git checkout.
*/
String gitDescribe = describeGitVersion()
version = project.findProperty('projectVersion') ?: (gitDescribe ? gitDescribe.substring(1) : 'unknown')
String describeGitVersion()
{
try
{
def describe = providers.exec {
commandLine 'git', 'describe', '--tags', '--match', 'v[0-9]*', '--dirty'
ignoreExitValue = true
}
if(describe.result.get().exitValue == 0)
{
return describe.standardOutput.asText.get().trim()
}
}
catch(Exception e)
{
logger.warn("Unable to determine version from git tags: " + e.message)
}
return null
}
tasks.register('printVersion') {
description = 'Prints the project version'
def projectVersion = version
doLast {
println projectVersion
}
}
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 27 is required for this version of the Project Panama preview/incubator feature
java {
@@ -131,53 +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 'com.formdev:flatlaf:3.8'
implementation 'com.formdev:flatlaf-intellij-themes:3.8' //Not imported - Theme.java loads these themes by class name
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.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.3' //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
@@ -238,7 +215,6 @@ jar {
attributes (
'Implementation-Title' : 'sdrtrunk project',
'Implementation-Version' : archiveVersion,
'Git-Describe' : gitDescribe ?: 'unknown',
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Timestamp' : new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
@@ -269,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
*/
@@ -297,138 +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')))
}
/**
* 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 folder for the target
* @return number of bytes removed from the jar
*/
long stripJar(File jar, String keep) {
def tunerPattern = ~/^[^\/]+\/native\/([^\/]+)\/.*/
def removed = [] as Set
def hadTuner = false, keptTuner = 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)
if(tuner.matches()) {
hadTuner = true
if(tuner.group(1) == keep) { keptTuner = true } else { removed << e.name }
}
}
if(hadTuner && !keptTuner) {
throw new GradleException("Native strip: ${jar.name} has no native/${keep} 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. The tuner libraries (jRTL, jAirspy, etc.) package their
* native libraries under '<library>/native/<os>-<arch>/', where the folder name matches the runtime target name
* '<os>-<arch>-v<version>' without the version suffix, except that the libraries use 'macos' where the target uses 'osx'.
*/
tasks.named('runtime') {rt ->
rt.doLast {
rt.targetPlatforms.get().keySet().each { target ->
def nativeFolder = (target - "-v${version}").replaceFirst(/^osx-/, 'macos-')
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, nativeFolder)
}
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.
*
@@ -485,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)
@@ -507,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

@@ -17,16 +17,15 @@
# *****************************************************************************
#
# The project version is no longer defined here. It is derived from the most recent 'v<version>' git tag (see
# build.gradle) and can be overridden with -PprojectVersion=<version>.
# Version naming conventions:
# Alpha: maj.min.patch-alpha-build
# Beta: maj.min.patch-beta-build
# Release: maj.min.patch
#
# Version tag naming conventions:
# Alpha: vmaj.min.patch-alpha-build
# Beta: vmaj.min.patch-beta-build
# Release: vmaj.min.patch
# Example: 0.6.0-beta-5
#
# Example: v0.6.0-beta-5
#
# Note: this is so that the file names for the resultant release build products match the GitHub release asset
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
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%

1023
sdr-trunk.ipr Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,483 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.audio.convert.thumbdv;
import com.fazecast.jSerialComm.SerialPort;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessage;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessageFactory;
import io.github.dsheirer.audio.convert.thumbdv.message.VocoderRate;
import io.github.dsheirer.audio.convert.thumbdv.message.request.AmbeRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.request.DecodeSpeechRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.request.EncodeSpeechRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.request.ResetRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.request.SetVocoderParametersRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.request.SetVocoderRequest;
import io.github.dsheirer.audio.convert.thumbdv.message.response.DecodeSpeechResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Northwest Digital Radio (NWDR) ThumbDv dongle.
*
* Note: linux users must allow access to the serial port:
*
* sudo usermod -a -G uucp username
* sudo usermod -a -G dialout username
* sudo usermod -a -G lock username
* sudo usermod -a -G tty username
*/
public class ThumbDv implements AutoCloseable
{
private final static Logger mLog = LoggerFactory.getLogger(ThumbDv.class);
private static final String PORT_DESCRIPTION = "USB-to-Serial Port (ftdi_sio)";
private static final String PORT_DESCRIPTION_FRAGMENT = "USB Serial Port";
private static final int BAUD_RATE = 460800;
private static final byte PACKET_START = (byte) 0x61;
public enum AudioProtocol
{
DMR,
DSTAR,
NXDN,
P25_PHASE2,
}
private SerialPort mSerialPort;
private ScheduledFuture mSerialPortReaderHandle;
private ScheduledFuture mAudioDecodeProcessorHandle;
private LinkedTransferQueue<DecodeSpeechRequest> mDecodeSpeechRequests = new LinkedTransferQueue<>();
private AudioProtocol mAudioProtocol;
private Listener<float[]> mAudioBufferListener;
private boolean mStarted;
public ThumbDv(AudioProtocol audioProtocol, Listener<float[]> listener)
{
mAudioProtocol = audioProtocol;
mAudioBufferListener = listener;
}
/**
* Enqueues the audio code frame for decoding. Decoded PCM speech packet will be sent to the registered
* audio packet listener.
*
* @param codecFrame
*/
public void decode(byte[] codecFrame)
{
if(!mStarted || mSerialPort == null)
{
throw new IllegalStateException("Must invoke start() method and thumbdv serial device must be available");
}
mDecodeSpeechRequests.offer(new DecodeSpeechRequest(codecFrame));
}
public void close() throws IOException
{
if(mSerialPortReaderHandle != null)
{
mSerialPortReaderHandle.cancel(true);
mSerialPortReaderHandle = null;
}
if(mSerialPort != null)
{
mSerialPort.closePort();
}
}
public void start() throws IOException
{
if(mSerialPort == null)
{
SerialPort[] ports = SerialPort.getCommPorts();
for(SerialPort port : ports)
{
mLog.debug("Serial Port Name:" + port.getDescriptivePortName());
if(port.getDescriptivePortName().contentEquals(PORT_DESCRIPTION) ||
port.getDescriptivePortName().contains(PORT_DESCRIPTION_FRAGMENT))
{
mSerialPort = port;
mSerialPort.setBaudRate(BAUD_RATE);
mSerialPort.openPort(0, 5000, 10000);
if(mSerialPort.isOpen())
{
mLog.info("Resetting ThumbDv Device");
send(new ResetRequest());
mLog.info("Creating Serial Port Reader");
final Runnable r = new SerialPortReader(mSerialPort.getInputStream());
mLog.info("Starting Serial Port Reader");
mSerialPortReaderHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(r, 0,
5, TimeUnit.MILLISECONDS);
mStarted = true;
mLog.info("Startup complete - awaiting reset response");
}
else
{
mLog.warn("Could not open serial port: " + mSerialPort.getSystemPortName());
throw new IOException("Could not open serial port:" + mSerialPort.getSystemPortName());
}
break;
}
}
}
if(mSerialPort == null)
{
throw new IOException("ThumbDV serial port not found");
}
return;
}
public void send(byte[] message) throws IOException
{
if(mSerialPort == null)
{
throw new IOException("ThumbDv must be started before use");
}
int bytesWritten = mSerialPort.writeBytes(message, message.length);
if(bytesWritten < 0)
{
throw new IOException("Unable to write message:" + Arrays.toString(message));
}
else if(bytesWritten != message.length)
{
throw new IOException("Unable to write message:" + Arrays.toString(message) + " Bytes Written:" + bytesWritten);
}
}
/**
* Sends the AMBE request message
*
* @param request message
* @throws IOException if there is an error
*/
public void send(AmbeRequest request) throws IOException
{
send(request.getData());
}
private void receive(byte[] bytes)
{
AmbeMessage message = AmbeMessageFactory.getMessage(bytes);
if(message instanceof DecodeSpeechResponse && mAudioBufferListener != null)
{
float[] samples = ((DecodeSpeechResponse)message).getSamples();
mAudioBufferListener.receive(samples);
}
else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null)
{
if(mAudioDecodeProcessorHandle == null)
{
try
{
switch(mAudioProtocol)
{
case DSTAR:
send(new SetVocoderRequest(VocoderRate.RATE_33));
send(new SetVocoderParametersRequest(0x0130, 0x0763, 0x4000, 0x0000, 0x0000, 0x0048));
break;
case DMR:
case NXDN:
case P25_PHASE2:
send(new SetVocoderRequest(VocoderRate.RATE_33));
send(new SetVocoderParametersRequest(0x0431, 0x0754, 0x2400, 0x0000, 0x0000, 0x6F48));
break;
default:
throw new IllegalStateException("Unrecognized audio protocol:" + mAudioProtocol);
}
}
catch(IOException ioe)
{
mLog.error("Error setting audio protocol vocoder parameters");
}
mLog.info("ThumbDv Reset Complete");
}
else
{
mLog.info("ThumbDv Reset Detected");
}
}
else if(message instanceof SetVocoderParameterResponse)
{
if(((SetVocoderParameterResponse)message).isSuccessful())
{
mLog.info("Audio vocoder parameters configured for " + mAudioProtocol);
//Start the audio frame decode processor
mAudioDecodeProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioDecodeProcessor(), 0,
10, TimeUnit.MILLISECONDS);
}
}
else if(message != null)
{
mLog.debug("RECEIVED:" + message.toString());
}
}
/**
* Logs the current settings of the serial port. Note: you must invoke start() before this method so that
* the serial port can be discovered and opened.
*/
public void logSerialPort()
{
if(mSerialPort != null)
{
StringBuilder sb = new StringBuilder();
sb.append("\nPort:\t\t\t").append(mSerialPort.getSystemPortName()).append("\n");
sb.append("Name:\t\t\t").append(mSerialPort.getDescriptivePortName()).append("\n");
sb.append("Baud Rate:\t\t").append(mSerialPort.getBaudRate()).append("\n");
sb.append("Data Bits:\t\t").append(mSerialPort.getNumDataBits()).append("\n");
sb.append("Parity Bits:\t").append(mSerialPort.getParity()).append("\n");
sb.append("Stop Bits:\t\t").append(mSerialPort.getNumStopBits()).append("\n");
sb.append("Flow Control:\t").append(mSerialPort.getFlowControlSettings()).append("\n");
sb.append("Is Open:\t\t").append(mSerialPort.isOpen()).append("\n");
mLog.info(sb.toString());
}
else
{
mLog.info("No serial port found");
}
}
/**
* Processes the audio frame decode queue
*/
public class AudioDecodeProcessor implements Runnable
{
@Override
public void run()
{
try
{
DecodeSpeechRequest request = mDecodeSpeechRequests.poll();
if(request != null)
{
try
{
send(request);
}
catch(IOException ioe)
{
mLog.error("Error decoding audio frame", ioe);
}
// try
// {
// //Force a 20ms sleep after submitting an audio frame to avoid overflowing the thumbdv
// Thread.sleep(20);
// }
// catch(InterruptedException ie)
// {
// //Do nothing
// }
}
}
catch(Throwable t)
{
mLog.error("Error", t);
}
}
}
public class SerialPortReader implements Runnable
{
private InputStream mInputStream;
public SerialPortReader(InputStream inputStream)
{
mInputStream = inputStream;
}
@Override
public void run()
{
try
{
if(mInputStream != null)
{
try
{
while(mInputStream.available() > 0)
{
byte[] buffer = new byte[400];
int bytesRead = mInputStream.readNBytes(buffer, 0, 1);
if(bytesRead == 1 && buffer[0] == PACKET_START)
{
while(mInputStream.available() < 2)
{
try
{
Thread.sleep(1);
}
catch(InterruptedException ie)
{
}
}
bytesRead = mInputStream.readNBytes(buffer, 1, 2);
if(bytesRead == 2)
{
int length = (0xFF & buffer[1]) << 8;
length += (0xFF & buffer[2]);
if(0 < length && length < 400)
{
length++; //Add a byte for the type byte
while(mInputStream.available() < length)
{
try
{
Thread.sleep(1);
}
catch(InterruptedException ie)
{
}
}
bytesRead = mInputStream.readNBytes(buffer, 3, length);
if(bytesRead == length)
{
receive(Arrays.copyOf(buffer, length + 3));
}
else
{
mLog.debug("Expected [" + length + "] but received [" + bytesRead + "] bytes - " + Arrays.toString(buffer));
}
}
else
{
mLog.error("Received packet with unexpected length: " + length);
//Don't process the buffer ... let the reader read and inspect 1 byte at a time
//to regain sync on the packet start byte
}
}
else
{
mLog.debug("Expected [2] but received [" + bytesRead + "] -- this shouldn't happen");
}
}
else
{
mLog.debug("Unrecognized byte: " + Arrays.toString(buffer));
}
}
}
catch(IOException ioe)
{
mLog.error("Error while reading ThumbDv serial port", ioe);
}
}
}
catch(Throwable t)
{
mLog.error("Error", t);
}
}
}
public static void main(String[] args)
{
mLog.debug("Starting");
String silence = "BEDDEA821EFD660C08";
String[] frames = {"0E46122323067C60F8", "0E469433C1067CF1BC", "0E46122B23067C60F8", "0E67162BE08874E2B4",
"0E46163BE1067CF1BC", "0E46122B23067C60F8", "0A06163BE00A5C303E", "0E46122B23067C60F8", "0E46163BE1847CE1FC",
"0E46122B23067C60F8"};
List<byte[]> frameData = new ArrayList<>();
for(String frame : frames)
{
byte[] bytes = new byte[frame.length() / 2];
for(int x = 0; x < frame.length(); x += 2)
{
String hex = frame.substring(x, x + 2);
bytes[x / 2] = (byte) (0xFF & Integer.parseInt(hex, 16));
}
frameData.add(bytes);
}
mLog.debug("Starting thumb dv thread(s)");
final Listener<float[]> listener = packet -> {
mLog.info("Got an audio packet!");
};
try(ThumbDv thumbDv = new ThumbDv(AudioProtocol.P25_PHASE2, listener))
{
thumbDv.start();
Thread.sleep(6000);
for(int x = 0; x < 20; x++)
{
thumbDv.send(new EncodeSpeechRequest(new short[160]));
Thread.sleep(20);
}
// for(byte[] frame : frameData)
// {
// thumbDv.decode(frame);
// }
while(true);
}
catch(IOException ioe)
{
mLog.error("Error", ioe);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,44 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message;
/**
* AMBE-3000R Message
*/
public abstract class AmbeMessage
{
/**
* Converts the byte array to a hexadecimal string array
*/
public static String toHex(byte[] data)
{
StringBuilder sb = new StringBuilder();
for(byte b: data)
{
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,95 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message;
import io.github.dsheirer.audio.convert.thumbdv.message.response.DecodeSpeechResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.EncodeSpeechResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.GetConfigResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.InitializeCodecResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.ProductIdResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetChannelFormatResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetChannelResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetPacketModeResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetSpeechFormatResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.UnknownResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.VersionResponse;
public class AmbeMessageFactory
{
private static final byte CONTROL_PACKET = (byte)0x00;
private static final byte CHANNEL_PACKET = (byte)0x01;
private static final byte SPEECH_PACKET = (byte)0x02;
private static final int INDEX_PACKET_TYPE = 3;
private static final int INDEX_CONTROL_PACKET_TYPE = 4;
public static AmbeMessage getMessage(byte[] data)
{
if(data != null && data.length >= 5)
{
if(data[INDEX_PACKET_TYPE] == CONTROL_PACKET)
{
PacketField packetField = PacketField.fromValue(data[INDEX_CONTROL_PACKET_TYPE]);
switch(packetField)
{
case PKT_CHANNEL_0:
return new SetChannelResponse(data);
case PKT_CHANNEL_FORMAT:
return new SetChannelFormatResponse(data);
case PKT_CODEC_STOP:
return new SetPacketModeResponse(data);
case PKT_GET_CONFIG:
return new GetConfigResponse(data);
case PKT_INIT:
return new InitializeCodecResponse(data);
case PKT_PRODUCT_ID:
return new ProductIdResponse(data);
case PKT_RATE_PARAMETER:
return new SetVocoderParameterResponse(data);
case PKT_RATE_TABLE:
return new SetVocoderResponse(data);
case PKT_READY:
return new ReadyResponse(data);
case PKT_SPEECH_FORMAT:
return new SetSpeechFormatResponse(data);
case PKT_VERSION_STRING:
return new VersionResponse(data);
}
}
else if(data[INDEX_PACKET_TYPE] == CHANNEL_PACKET)
{
return new EncodeSpeechResponse(data);
}
else if(data[INDEX_PACKET_TYPE] == SPEECH_PACKET)
{
return new DecodeSpeechResponse(data);
}
}
return new UnknownResponse(data);
}
}

View File

@@ -0,0 +1,47 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message;
/**
* AMBE-3000 Initialization Options
*/
public enum InitializeOption
{
ENCODER((byte)0x01),
DECODER((byte)0x02),
ENCODER_AND_DECODER((byte)0x03),
ECHO_CANCELLER((byte)0x04),
ENCODER_DECODER_ECHO_CANCELLER((byte)0x07);
private byte mCode;
InitializeOption(byte code)
{
mCode = code;
}
public byte getCode()
{
return mCode;
}
}

View File

@@ -0,0 +1,160 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.audio.convert.thumbdv.message;
/**
* AMBE-3000R Control Packet Field enumeration
*/
public enum PacketField
{
PKT_ENCODER_CMODE((byte)0x05),
PKT_DECODER_CMODE((byte)0x06),
PKT_RATE_TABLE((byte)0x09),
PKT_RATE_PARAMETER((byte)0x0A),
PKT_INIT((byte)0x0B),
PKT_LOW_POWER((byte)0x10),
PKT_CHANNEL_FORMAT((byte)0x15),
PKT_SPEECH_FORMAT((byte)0x16),
PKT_CODEC_START((byte)0x2A),
PKT_CODEC_STOP((byte)0x2B),
PKT_PRODUCT_ID((byte)0x30),
PKT_VERSION_STRING((byte)0x31),
PKT_COMPAND((byte)0x32),
PKT_RESET((byte)0x33),
PKT_RESET_TO_SOFTWARE_CONFIG((byte)0x34),
PKT_HALT((byte)0x35),
PKT_GET_CONFIG((byte)0x36),
PKT_READ_CONFIG((byte)0x37),
PKT_CODEC_CFG((byte)0x38),
PKT_READY((byte)0x39),
PKT_PARITY_MODE((byte)0x3F),
PKT_CHANNEL_0((byte)0x40),
PKT_WRITE_I2C((byte)0x44),
PKT_CLEAR_CODEC_RESET((byte)0x46),
PKT_SET_CODEC_RESET((byte)0x47),
PKT_DISCARD_CODEC_SAMPLES((byte)0x48),
PKT_DELAY_NUMBER_MICRO_SECONDS((byte)0x49),
PKT_DELAY_NUMBER_NANO_SECONDS((byte)0x4A),
PKT_GAIN((byte)0x4B),
PKT_RTS_THRESHOLD((byte)0x4E),
//Speech packet values
SPEECH_DATA((byte)0x00),
CHANNEL_DATA_HARD_SYMBOL((byte)0x01),
CMODE((byte)0x02),
SAMPLE_COUNT((byte)0x03),
TONE((byte)0x08),
CHANNEL_DATA_SOFT_SYMBOL((byte)0x17),
VOCODER((byte)0x40),
//Packet Types
PACKET_TYPE_CONTROL((byte)0x00),
PACKET_TYPE_ENCODE_SPEECH((byte)0x01),
PACKET_TYPE_DECODE_SPEECH((byte)0x02),
UNKNOWN((byte)0x00);
private byte mCode;
PacketField(byte code)
{
mCode = code;
}
/**
* Packet type byte code value
*/
public byte getCode()
{
return mCode;
}
/**
* Lookup a packet type from the code byte value
*/
public static PacketField fromValue(byte value)
{
switch(value)
{
case (byte)0x05:
return PKT_ENCODER_CMODE;
case (byte)0x06:
return PKT_DECODER_CMODE;
case (byte)0x09:
return PKT_RATE_TABLE;
case (byte)0x0A:
return PKT_RATE_PARAMETER;
case (byte)0x0B:
return PKT_INIT;
case (byte)0x10:
return PKT_LOW_POWER;
case (byte)0x15:
return PKT_CHANNEL_FORMAT;
case (byte)0x16:
return PKT_SPEECH_FORMAT;
case (byte)0x2A:
return PKT_CODEC_START;
case (byte)0x2B:
return PKT_CODEC_STOP;
case (byte)0x30:
return PKT_PRODUCT_ID;
case (byte)0x31:
return PKT_VERSION_STRING;
case (byte)0x32:
return PKT_COMPAND;
case (byte)0x33:
return PKT_RESET;
case (byte)0x34:
return PKT_RESET_TO_SOFTWARE_CONFIG;
case (byte)0x35:
return PKT_HALT;
case (byte)0x36:
return PKT_GET_CONFIG;
case (byte)0x37:
return PKT_READ_CONFIG;
case (byte)0x38:
return PKT_CODEC_CFG;
case (byte)0x39:
return PKT_READY;
case (byte)0x3F:
return PKT_PARITY_MODE;
case (byte)0x40:
return PKT_CHANNEL_0;
case (byte)0x44:
return PKT_WRITE_I2C;
case (byte)0x46:
return PKT_CLEAR_CODEC_RESET;
case (byte)0x47:
return PKT_SET_CODEC_RESET;
case (byte)0x48:
return PKT_DISCARD_CODEC_SAMPLES;
case (byte)0x49:
return PKT_DELAY_NUMBER_MICRO_SECONDS;
case (byte)0x4A:
return PKT_DELAY_NUMBER_NANO_SECONDS;
case (byte)0x4B:
return PKT_GAIN;
case (byte)0x4E:
return PKT_RTS_THRESHOLD;
default:
return UNKNOWN;
}
}
}

View File

@@ -0,0 +1,129 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message;
public enum VocoderRate
{
//AMBE-1000 Standard Rates
RATE_0((byte)0x00, 2400,0),
RATE_1((byte)0x01, 3600,0),
RATE_2((byte)0x02, 3600,1200),
RATE_3((byte)0x03, 4800,0),
RATE_4((byte)0x04, 9600,0),
RATE_5((byte)0x05, 2350,50),
RATE_6((byte)0x06, 4850,4750),
RATE_7((byte)0x07, 4550,250),
RATE_8((byte)0x08, 3100,1700),
RATE_9((byte)0x09, 4400,2800),
RATE_10((byte)0x0A, 4150,2250),
RATE_11((byte)0x0B, 3350,250),
RATE_12((byte)0x0C, 7750,250),
RATE_13((byte)0x0D, 4650,3350),
RATE_14((byte)0x0E, 3750,250),
RATE_15((byte)0x0F, 4000,0),
//AMBE-2000 Standard Rates
RATE_16((byte)0x10, 3600,0),
RATE_17((byte)0x11, 4000,0),
RATE_18((byte)0x12, 4800,0),
RATE_19((byte)0x13, 6400,0),
RATE_20((byte)0x14, 8000,0),
RATE_21((byte)0x15, 9600,0),
RATE_22((byte)0x16, 2400,1600),
RATE_23((byte)0x17, 3600,1200),
RATE_24((byte)0x18, 4000,800),
RATE_25((byte)0x19, 2400,2400),
RATE_26((byte)0x1A, 4000,2400),
RATE_27((byte)0x1B, 4400,2800), //Is this compatible with APCO25 Phase 1?
RATE_28((byte)0x1C, 4000,4000),
RATE_29((byte)0x1D, 2400,7200),
RATE_30((byte)0x1E, 3600,6000),
RATE_31((byte)0x1F, 2000,0),
RATE_32((byte)0x20, 3600,2800),
//AMBE-3000 Standard Rates (AMBE+2)
RATE_33((byte)0x21, 2450,1150), //APCO25 HR (Phase 2), DMR, NXDN, DSTAR
RATE_34((byte)0x22, 2450,0),
RATE_35((byte)0x23, 2250,1150),
RATE_36((byte)0x24, 2250,0),
RATE_37((byte)0x25, 2400,0),
RATE_38((byte)0x26, 3000,0),
RATE_39((byte)0x27, 3600,0),
RATE_40((byte)0x28, 4000,0),
RATE_41((byte)0x29, 4400,0),
RATE_42((byte)0x2A, 4800,0),
RATE_43((byte)0x2B, 6400,0),
RATE_44((byte)0x2C, 7200,0),
RATE_45((byte)0x2D, 8000,0),
RATE_46((byte)0x2E, 9600,0),
RATE_47((byte)0x2F, 2450,250),
RATE_48((byte)0x30, 3350,250),
RATE_49((byte)0x31, 3750,250),
RATE_50((byte)0x32, 4550,250),
RATE_51((byte)0x33, 2450,1950),
RATE_52((byte)0x34, 2450,2350),
RATE_53((byte)0x35, 2450,3550),
RATE_54((byte)0x36, 2450,4750),
RATE_55((byte)0x37, 2600,1400),
RATE_56((byte)0x38, 3600,1200),
RATE_57((byte)0x39, 4000,800),
RATE_58((byte)0x3A, 4000,2400),
RATE_59((byte)0x3B, 4400,2800), //Is this compatible with APCO25 Phase 1?
RATE_60((byte)0x3C, 4000,4000),
RATE_61((byte)0x3D, 3600,6000);
private byte mCode;
private int mSpeechRate;
private int mFecRate;
VocoderRate(byte code, int speechRate, int fecRate)
{
mCode = code;
mSpeechRate = speechRate;
mFecRate = fecRate;
}
/**
* Byte code value for the vocoder rate
*/
public byte getCode()
{
return mCode;
}
/**
* Speech rate in bits per second (bps)
*/
public int getSpeechRate()
{
return mSpeechRate;
}
/**
* Forward Error Correction rate in bits per second (bps)
*/
public int getFecRate()
{
return mFecRate;
}
}

View File

@@ -0,0 +1,67 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessage;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* AMBE-3000R Request Packet
*/
public abstract class AmbeRequest extends AmbeMessage
{
private static final byte PACKET_START_BYTE = (byte)0x61;
protected static final int PACKET_TYPE_INDEX = 3;
protected static final int PAYLOAD_START_INDEX = 4;
public abstract PacketField getType();
public abstract byte[] getData();
/**
* Creates a byte array of the specified length plus 4 packet header bytes with packet start, length and control
* bytes filled in.
*
* @param length of the message (not including the 4 byte packet header)
* @return byte array with packet header pre-filled.
*/
protected byte[] createMessage(int length, PacketField type)
{
byte[] data = new byte[length + 4];
data[0] = PACKET_START_BYTE;
data[1] = (byte)((length >> 8 & 0xFF));
data[2] = (byte)(length & 0xFF);
if(type == PacketField.PACKET_TYPE_ENCODE_SPEECH || type == PacketField.PACKET_TYPE_DECODE_SPEECH)
{
data[PACKET_TYPE_INDEX] = type.getCode();
}
else
{
data[PACKET_TYPE_INDEX] = PacketField.PACKET_TYPE_CONTROL.getCode();
data[PAYLOAD_START_INDEX] = type.getCode();
}
return data;
}
}

View File

@@ -0,0 +1,108 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.audio.convert.thumbdv.message.VocoderRate;
/**
* Decode speech request, used to request decode of an encoded audio frame.
*/
public class DecodeSpeechRequest extends AmbeRequest
{
private static final int CHANNEL_DATA_IDENTIFIER_INDEX = 4;
private static final byte SAMPLE_COUNT = (byte)(0xFF & 160); //8 kHz Audio sample count for 20ms frame
private byte[] mAudioFrame;
private VocoderRate mVocoderRate;
/**
* Constructs an audio frame decode request using the specified vocoder rate.
* @param audioFrame of encoded audio samples
* @param vocoderRate to use when decoding
*/
public DecodeSpeechRequest(byte[] audioFrame, VocoderRate vocoderRate)
{
mAudioFrame = audioFrame;
mVocoderRate = vocoderRate;
}
/**
* Constructs an audio frame decode request using the current vocoder rate.
* @param audioFrame of encoded audio samples
*/
public DecodeSpeechRequest(byte[] audioFrame)
{
this(audioFrame, null);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_ENCODE_SPEECH;
}
private boolean hasVocoderRate()
{
return mVocoderRate != null;
}
@Override
public byte[] getData()
{
if(hasVocoderRate())
{
int length = mAudioFrame.length + 9;
byte[] data = createMessage(length, getType());
int offset = CHANNEL_DATA_IDENTIFIER_INDEX;
data[offset++] = PacketField.VOCODER.getCode();
data[offset++] = mVocoderRate.getCode();
data[offset++] = PacketField.CHANNEL_DATA_HARD_SYMBOL.getCode();
data[offset++] = (byte)(0xFF & (mAudioFrame.length * 8));
System.arraycopy(mAudioFrame, 0, data, offset, mAudioFrame.length);
offset += mAudioFrame.length;
data[offset++] = PacketField.SAMPLE_COUNT.getCode();
data[offset++] = (byte)0xA0;
data[offset++] = (byte)0x02;
data[offset++] = (byte)0x00;
data[offset] = (byte)0x00;
return data;
}
else
{
int length = mAudioFrame.length + 2;
byte[] data = createMessage(length, getType());
int offset = CHANNEL_DATA_IDENTIFIER_INDEX;
data[offset++] = PacketField.CHANNEL_DATA_HARD_SYMBOL.getCode();
data[offset++] = (byte)(0xFF & (mAudioFrame.length * 8));
System.arraycopy(mAudioFrame, 0, data, offset, mAudioFrame.length);
return data;
}
}
}

View File

@@ -0,0 +1,88 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
public class EncodeSpeechRequest extends AmbeRequest
{
private static final int CHANNEL_IDENTIFIER_INDEX = 4;
private static final int SPEECH_DATA_IDENTIFIER_INDEX = 5;
private static final int SAMPLE_COUNT_INDEX = 6;
private static final int SPEECH_DATA_START_INDEX = 7;
private short[] mSamples;
public EncodeSpeechRequest(short[] samples)
{
mSamples = samples;
}
public EncodeSpeechRequest(float[] samples)
{
mSamples = new short[samples.length];
for(int x = 0; x < samples.length; x++)
{
if(samples[x] > 1.0f)
{
mSamples[x] = Short.MAX_VALUE;
}
else if(samples[x] < -1.0f)
{
mSamples[x] = Short.MIN_VALUE;
}
else
{
mSamples[x] = (short)(samples[x] * Short.MAX_VALUE);
}
}
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_DECODE_SPEECH;
}
@Override
public byte[] getData()
{
int length = (mSamples.length * 2) + 3;
byte[] data = createMessage(length, getType());
data[CHANNEL_IDENTIFIER_INDEX] = PacketField.PKT_CHANNEL_0.getCode();
data[SPEECH_DATA_IDENTIFIER_INDEX] = (byte)0x00;
data[SAMPLE_COUNT_INDEX] = (byte)(0xFF & mSamples.length);
int pointer = SPEECH_DATA_START_INDEX;
for(short sample: mSamples)
{
data[pointer++] = (byte)((sample >> 8 & 0xFF));
data[pointer++] = (byte)(sample & 0xFF);
}
return data;
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Get configuration request packet
*/
public class GetConfigRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_GET_CONFIG;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,54 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.InitializeOption;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Initialize CODEC request packet
*/
public class InitializeCodecRequest extends AmbeRequest
{
private static final int INITIALIZE_OPTION_INDEX = 5;
private InitializeOption mInitializeOption;
public InitializeCodecRequest(InitializeOption option)
{
mInitializeOption = option;
}
@Override
public PacketField getType()
{
return PacketField.PKT_INIT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[INITIALIZE_OPTION_INDEX] = mInitializeOption.getCode();
return data;
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Product ID request packet
*/
public class ProductIdRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_PRODUCT_ID;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Reset request packet
*/
public class ResetRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_RESET;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,48 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel format request packet
*/
public class SetChannelFormatRequest extends AmbeRequest
{
private static final int FORMAT_INDEX = 5;
private static final byte FORMAT = (byte)0x00;
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_FORMAT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[FORMAT_INDEX] = FORMAT;
return data;
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel request packet
*/
public class SetChannelRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_0;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Packet mode (versus CODEC mode) request packet
*/
public class SetPacketModeRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_CODEC_STOP;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,48 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set speech format request packet
*/
public class SetSpeechFormatRequest extends AmbeRequest
{
private static final int FORMAT_INDEX = 5;
private static final byte FORMAT = (byte)0x00;
@Override
public PacketField getType()
{
return PacketField.PKT_SPEECH_FORMAT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[FORMAT_INDEX] = FORMAT;
return data;
}
}

View File

@@ -0,0 +1,74 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder parameters request packet
*/
public class SetVocoderParametersRequest extends AmbeRequest
{
private int mWord0;
private int mWord1;
private int mWord2;
private int mWord3;
private int mWord4;
private int mWord5;
public SetVocoderParametersRequest(int word0, int word1, int word2, int word3, int word4, int word5)
{
mWord0 = word0;
mWord1 = word1;
mWord2 = word2;
mWord3 = word3;
mWord4 = word4;
mWord5 = word5;
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_PARAMETER;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(14, getType());
data[PAYLOAD_START_INDEX + 1] = (byte)(mWord0 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 2] = (byte)(mWord0 & 0xFF);
data[PAYLOAD_START_INDEX + 3] = (byte)(mWord1 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 4] = (byte)(mWord1 & 0xFF);
data[PAYLOAD_START_INDEX + 5] = (byte)(mWord2 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 6] = (byte)(mWord2 & 0xFF);
data[PAYLOAD_START_INDEX + 7] = (byte)(mWord3 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 8] = (byte)(mWord3 & 0xFF);
data[PAYLOAD_START_INDEX + 9] = (byte)(mWord4 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 10] = (byte)(mWord4 & 0xFF);
data[PAYLOAD_START_INDEX + 11] = (byte)(mWord5 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 12] = (byte)(mWord5 & 0xFF);
return data;
}
}

View File

@@ -0,0 +1,53 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.audio.convert.thumbdv.message.VocoderRate;
/**
* Set Vocoder Rate request packet
*/
public class SetVocoderRequest extends AmbeRequest
{
private VocoderRate mVocoderRate;
public SetVocoderRequest(VocoderRate rate)
{
mVocoderRate = rate;
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_TABLE;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[PAYLOAD_START_INDEX + 1] = mVocoderRate.getCode();
return data;
}
}

View File

@@ -0,0 +1,43 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Version string request packet
*/
public class VersionRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_VERSION_STRING;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -0,0 +1,60 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessage;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* AMBE-3000R Response Packet
*/
public abstract class AmbeResponse extends AmbeMessage
{
protected static final int PAYLOAD_START_INDEX = 5;
private byte[] mMessage;
protected AmbeResponse(byte[] message)
{
mMessage = message;
}
/**
* Control packet type
*/
public abstract PacketField getType();
/**
* Received message bytes
*/
protected byte[] getMessage()
{
return mMessage;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX, getMessage().length);
}
}

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.sample.ConversionUtils;
import java.util.Arrays;
/**
* Decode speech response
*/
public class DecodeSpeechResponse extends AmbeResponse
{
public DecodeSpeechResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_DECODE_SPEECH;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX + 1, getMessage().length);
}
public float[] getSamples()
{
return ConversionUtils.convertFromSigned16BitSamples(getPayload());
}
@Override
public String toString()
{
return "DECODED SPEECH: SAMPLE COUNT:" + getSamples().length + " " + toHex(getPayload());
}
}

View File

@@ -0,0 +1,59 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Encode speech response
*/
public class EncodeSpeechResponse extends AmbeResponse
{
public EncodeSpeechResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_ENCODE_SPEECH;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX + 1, getMessage().length);
}
@Override
public String toString()
{
return "ENCODED SPEECH: " + toHex(getPayload()) + " " + Arrays.toString(getMessage());
}
}

View File

@@ -0,0 +1,49 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Get configuration response
*/
public class GetConfigResponse extends AmbeResponse
{
public GetConfigResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_GET_CONFIG;
}
@Override
public String toString()
{
return "CONFIGURATION: " + toHex(getPayload());
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Initialize CODEC response
*/
public class InitializeCodecResponse extends AmbeResponse
{
public InitializeCodecResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_INIT;
}
/**
* Success / fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "INITIALIZE CODEC " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,56 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Product ID Response
*/
public class ProductIdResponse extends AmbeResponse
{
public ProductIdResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_PRODUCT_ID;
}
/**
* Product ID string
*/
public String getProductId()
{
return new String(getPayload()).trim();
}
@Override
public String toString()
{
return "PRODUCT ID:" + getProductId();
}
}

View File

@@ -0,0 +1,50 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Ready response
*/
public class ReadyResponse extends AmbeResponse
{
public ReadyResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_READY;
}
@Override
public String toString()
{
return "READY" + Arrays.toString(getMessage());
}
}

View File

@@ -0,0 +1,57 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Reset response
*/
public class ResetResponse extends AmbeResponse
{
public ResetResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RESET;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "RESET " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel format response
*/
public class SetChannelFormatResponse extends AmbeResponse
{
public SetChannelFormatResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_FORMAT;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET CHANNEL FORMAT " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,57 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder rate response
*/
public class SetChannelResponse extends AmbeResponse
{
public SetChannelResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_0;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET CHANNEL 0 " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set packet mode response
*/
public class SetPacketModeResponse extends AmbeResponse
{
public SetPacketModeResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CODEC_STOP;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET PACKET MODE " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set speech format response
*/
public class SetSpeechFormatResponse extends AmbeResponse
{
public SetSpeechFormatResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_SPEECH_FORMAT;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET SPEECH FORMAT " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,65 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder parameter response
*/
public class SetVocoderParameterResponse extends AmbeResponse
{
public SetVocoderParameterResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_PARAMETER;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==3 && payload[0] == 0;
}
@Override
public String toString()
{
if(isSuccessful())
{
return "SET VOCODER RATE SUCCESSFUL";
}
else
{
return "SET VOCODER RATE **FAILED** - RESPONSE:" + toHex(getPayload());
}
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder rate response
*/
public class SetVocoderResponse extends AmbeResponse
{
public SetVocoderResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_TABLE;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET VOCODER RATE " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -0,0 +1,50 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Unknown Response Message
*/
public class UnknownResponse extends AmbeResponse
{
public UnknownResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.UNKNOWN;
}
@Override
public String toString()
{
return "UNKNOWN MESSAGE:" + Arrays.toString(getMessage());
}
}

View File

@@ -0,0 +1,56 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Version Response
*/
public class VersionResponse extends AmbeResponse
{
public VersionResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_VERSION_STRING;
}
/**
* Version string
*/
public String getVersion()
{
return new String(getPayload()).trim();
}
@Override
public String toString()
{
return "VERSION:" + getVersion();
}
}

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();
}
}
@@ -1313,7 +1297,7 @@ public abstract class C4FMSymbolProcessor
for(int x = 0; x < patternSymbols.length; x++)
{
if(bufferPointer >= 0 && bufferPointer < maxPointer)
if(bufferPointer < maxPointer)
{
softSymbol = interpolate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional) * gain + balance;
}

View File

@@ -68,29 +68,6 @@ public class Golay24
*/
public static int checkAndCorrect(CorrectedBinaryMessage message, int startIndex)
{
//The correction below indexes the word from bit 0 and takes the parity over the whole message, so it is only
//correct for a message that is exactly the 24-bit word. Decode any other word in a 24-bit copy.
if(startIndex != 0 || message.size() != 24)
{
CorrectedBinaryMessage word = new CorrectedBinaryMessage(24);
int original = message.getInt(startIndex, startIndex + 23);
word.load(0, 24, original);
int result = checkAndCorrectWord(word);
int corrected = word.getInt(0, 23);
message.load(startIndex, 24, corrected);
message.incrementCorrectedBitCount(Integer.bitCount(original ^ corrected));
return result;
}
return checkAndCorrectWord(message);
}
/**
* Error detection and correction of a message that is exactly one 24-bit word.
*/
private static int checkAndCorrectWord(CorrectedBinaryMessage message)
{
int startIndex = 0;
boolean parityError = message.cardinality() % 2 != 0;
int syndrome = getSyndrome(message, startIndex);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -60,7 +60,6 @@ import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.DuplicateLibraryDetector;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
@@ -82,7 +81,6 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
@@ -155,17 +153,6 @@ public class SDRTrunk implements Listener<TunerEvent>
mApplicationLog = new ApplicationLog(mUserPreferences);
mApplicationLog.start();
Map<String,List<String>> duplicateLibraries = DuplicateLibraryDetector.findDuplicates();
if(!duplicateLibraries.isEmpty())
{
mLog.warn("More than one version of these libraries is in the library folder [" +
DuplicateLibraryDetector.getFolder(duplicateLibraries) + "]. This happens when a new version of " +
"sdrtrunk is unzipped over an older one, and it can load the older library code. Delete the " +
"sdrtrunk folder and unzip the release into an empty folder.\n" +
DuplicateLibraryDetector.describe(duplicateLibraries));
}
mResourceMonitor = new ResourceMonitor(mUserPreferences);
ThreadPool.logSettings();
@@ -266,11 +253,6 @@ public class SDRTrunk implements Listener<TunerEvent>
{
updateTitle(tuner.getPreferredName());
}
if(!duplicateLibraries.isEmpty())
{
showDuplicateLibrariesDialog(duplicateLibraries);
}
}
if(calibrating && !GraphicsEnvironment.isHeadless())
@@ -927,28 +909,6 @@ public class SDRTrunk implements Listener<TunerEvent>
}
/**
* Warns the user that the library folder holds more than one version of some libraries, which happens when a new
* release is unzipped over an older one. The launch scripts put every jar in the library folder on the class
* path, so the older library code can load instead of the newer code.
* @param duplicates library name to jar paths, from DuplicateLibraryDetector
*/
private void showDuplicateLibrariesDialog(Map<String,List<String>> duplicates)
{
String message = "<html><body style='width: 450px'>" +
"<p>The sdrtrunk library folder has more than one version of these libraries:</p><br>" +
"<pre>" + DuplicateLibraryDetector.describe(duplicates) + "</pre>" +
"<p>This happens when a new version of sdrtrunk is unzipped over an older one. sdrtrunk can load " +
"the older library code, which can cause crashes or errors that the newer version fixed.</p><br>" +
"<p>Close sdrtrunk, delete the sdrtrunk folder that contains this library folder and unzip the " +
"release into a new, empty folder:</p><br>" +
"<p><b>" + DuplicateLibraryDetector.getFolder(duplicates) + "</b></p><br>" +
"<p>Your playlist and settings are stored in your user folder and won't be affected.</p>" +
"</body></html>";
JOptionPane.showMessageDialog(mMainGui, message, "Duplicate Library Versions", JOptionPane.WARNING_MESSAGE);
}
/**
* Launch the application.
*/

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

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -133,7 +133,6 @@ public class ApplicationLog
Attributes atts = findManifestAttributes();
if (atts != null) {
mLog.info("SDRTrunk Version : " + atts.getValue("Implementation-Version"));
mLog.info("Git Describe : " + atts.getValue("Git-Describe"));
mLog.info("Gradle Version : " + atts.getValue("Created-By"));
mLog.info("Build Timestamp : " + atts.getValue("Build-Timestamp"));
mLog.info("Build-JDK : " + atts.getValue("Build-JDK"));

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

@@ -123,23 +123,25 @@ public class LRRPPacket extends Packet
}
else
{
String message = getMessage().getSubMessage(getOffset(), getOffset() + ((characterCount + 2) * 8)).toHexString();
int x = 0;
RequestedTokens requestedTokens = new RequestedTokens();
while(characterCount > 0 && offset < getMessage().size())
{
String tokenId = getTokenIdentifier(offset);
TokenType tokenType = TokenType.fromRequestValue(tokenId);
TokenType tokenType = TokenType.fromValue(tokenId);
if(tokenType.isRequestParameterToken())
{
Token token = TokenFactory.createToken(tokenType, getMessage(), offset, characterCount);
Token token = TokenFactory.createToken(tokenId, getMessage(), offset, characterCount);
mTokens.add(token);
characterCount -= token.getByteLength();
offset += (8 * token.getByteLength());
}
else
{
//Single-byte flag naming a field that the radio should include in its location report
if(tokenType != TokenType.UNKNOWN)
{
requestedTokens.add(tokenType);

View File

@@ -26,7 +26,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
* LRRP Identity Token
*
* Start Token: 0x22
* Total Length: 2 bytes plus a 1-4 byte request identifier
* Total Length: 6 bytes
*/
public class Identity extends Token
{
@@ -34,9 +34,9 @@ public class Identity extends Token
private static final int[] BYTE_VALUE = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
/**
* Constructs an instance of an identity token.
* Constructs an instance of a heading token.
*
* @param message containing the identity
* @param message containing the heading
* @param offset to the start of the token
*/
public Identity(CorrectedBinaryMessage message, int offset)
@@ -50,47 +50,47 @@ public class Identity extends Token
return TokenType.IDENTITY;
}
/**
* Request identifier bytes, as transmitted. The read is clamped to the end of the message so that a corrupted
* length field cannot produce bytes that were never received.
*/
private BinaryMessage getIdentifierBytes()
private BinaryMessage getTokenContents()
{
int start = getOffset() + IDENTIFIER_START;
int available = Math.max(0, getMessage().size() - start);
int length = Math.min(getVariableLengthPayloadSize() * 8, available);
return getMessage().getSubMessage(start, start + length);
return getMessage().getSubMessage(getOffset(), getOffset() + 48);
}
/**
* Request identifier assembled big-endian from the 1-4 payload bytes. Moto.Net (LRRPPacket.cs) and node-dmr-lib
* (LRRP.js) both read the identifier this way.
* Extracts the payload contents.
*
* @return identifier, or zero when the payload length is not 1-4 bytes.
* Note: assumes that the values are 1-4 bytes in big-endian format? Not sure if this is correct because the
* values that I observed only had value in the LSB and the 3 MSBs were all zeros.
*/
public int getID()
{
int length = getVariableLengthPayloadSize();
if(length < 1 || length > 4)
{
return 0;
}
int payloadOffset = 16;
int value = 0;
for(int x = 0; x < length; x++)
while(0 < length && length <= 4)
{
value = (value << 8) | getMessage().getInt(BYTE_VALUE, getOffset() + IDENTIFIER_START + (x * 8));
int byteValue = getMessage().getInt(BYTE_VALUE, getOffset() + payloadOffset);
byteValue = Integer.rotateLeft(byteValue, payloadOffset - 16);
value += byteValue;
payloadOffset += 8;
length--;
}
return value;
}
@Override
public String toString()
{
//Hexadecimal, as transmitted (e.g. ID:FFFFEE)
return "ID:" + getIdentifierBytes().toHexString();
int id = getID();
if(id != 0)
{
return "ID:" + id;
}
return "ID:" + getTokenContents().toHexString();
}
}

View File

@@ -1,67 +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.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP request parameter of unknown meaning that carries a 7-bit variable length integer value.
*
* Start Tokens: 0x42 and 0x44
* Total Length: 1 byte identifier plus a variable length integer value (0-5 bytes)
*
* Observed in Motorola P25 triggered location start requests as 0x44 0x64 (100) and 0x42 0x9C 0x10 (3600), placed
* among the requested-field flags. Moto.Net emits a bare 0x42 as a "trigger on GPIO" flag, so the value is treated
* as optional and the token is rendered without a value when none follows.
*/
public class RequestParameter extends VarIntToken
{
private TokenType mTokenType;
/**
* Constructs an instance of a request parameter token.
*
* @param tokenType of the parameter (REQUEST_PARAMETER_42 or REQUEST_PARAMETER_44)
* @param message containing the token
* @param offset to the start of the token
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
*/
public RequestParameter(TokenType tokenType, CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
{
super(message, offset, remainingCharacterCount);
mTokenType = tokenType;
}
@Override
public TokenType getTokenType()
{
return mTokenType;
}
@Override
public String toString()
{
if(hasValue())
{
return getTokenType() + ":" + getValue();
}
return getTokenType().toString();
}
}

View File

@@ -63,28 +63,12 @@ public class RequestedTokens extends Token
Collections.sort(mUnrecognizedRequestedTokens);
}
/**
* Recognized requested token types, in the order that they appeared in the packet.
*/
public List<TokenType> getRequestedTokens()
{
return mRequestedTokens;
}
/**
* Unrecognized requested token identifiers (hexadecimal), sorted.
*/
public List<String> getUnrecognizedRequestedTokens()
{
return mUnrecognizedRequestedTokens;
}
/**
* Indicates if this object has requested tokens.
*/
public boolean hasRequestedTokens()
{
return !mRequestedTokens.isEmpty() || !mUnrecognizedRequestedTokens.isEmpty();
return !mRequestedTokens.isEmpty();
}
@Override

View File

@@ -28,7 +28,7 @@ import java.util.TimeZone;
/**
* LRRP Timestamp Token
*
* Start Token: 0x34 (response packets; in a request packet 0x34 is the Periodic Trigger token)
* Start Token: 0x34
* Total Length: 6 bytes
*/
public class Timestamp extends Token

View File

@@ -88,11 +88,6 @@ public abstract class Token
{
return getVariableLengthPayloadSize() + 2; //Variable Length Payload + Id + Length Field
}
else if(getTokenType().isVarInt())
{
//Id + 7-bit variable length integer. VarIntToken overrides this to bound the read by the payload.
return VarInt.length(getMessage(), getOffset() + 8, VarInt.MAX_BYTES) + 1;
}
else
{
return getTokenType().getLength() + 1; //Fixed Length Payload + Id

View File

@@ -26,32 +26,10 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
*/
public class TokenFactory
{
/**
* Creates a token using the response packet meaning of the token identifier.
*
* @param id hexadecimal token identifier
* @param message containing the token
* @param offset to the start of the token identifier
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
* @return token
*/
public static Token createToken(String id, CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
{
return createToken(TokenType.fromValue(id), message, offset, remainingCharacterCount);
}
TokenType tokenType = TokenType.fromValue(id);
/**
* Creates a token of the specified type.
*
* @param tokenType resolved by the caller, using TokenType.fromRequestValue() for request packets
* @param message containing the token
* @param offset to the start of the token identifier
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
* @return token
*/
public static Token createToken(TokenType tokenType, CorrectedBinaryMessage message, int offset,
int remainingCharacterCount)
{
switch(tokenType)
{
case HEADING:
@@ -74,17 +52,14 @@ public class TokenFactory
return new Success(message, offset);
case TIMESTAMP:
return new Timestamp(message, offset);
case TRIGGER_CONDITION:
return new TriggerCondition(message, offset);
case TRIGGER_DISTANCE:
return new TriggerDistance(message, offset, remainingCharacterCount);
case TRIGGER_INTERVAL:
return new TriggerInterval(message, offset, remainingCharacterCount);
return new TriggerDistance(message, offset);
case TRIGGER_GPIO:
return new TriggerGpio(message, offset);
case TRIGGER_ON_MOVE:
return new TriggerOnMove(message, offset);
case TRIGGER_PERIODIC:
return new TriggerPeriodic(message, offset);
case REQUEST_PARAMETER_42:
case REQUEST_PARAMETER_44:
return new RequestParameter(tokenType, message, offset, remainingCharacterCount);
case VERSION:
return new Version(message, offset);
case UNKNOWN_23:

View File

@@ -26,37 +26,21 @@ import java.util.Map;
/**
* LRRP Token Types
*
* Fixed length tokens consist of an identifier followed by a fixed number of value bytes. Variable length tokens
* consist of an identifier, a length indicator, and then a sequence of bytes equal in length to the length indicator.
* Varint tokens consist of an identifier followed by a 7-bit variable length integer (see VarInt).
*
* A few identifiers have a different meaning in a request than in a response (0x34 is the periodic trigger flag in a
* request and the timestamp in a response). Use fromRequestValue() for request packets and fromValue() otherwise.
*
* Note: the declaration order (ordinal) controls the order that tokens are listed when a packet is rendered as text.
* Variable length tokens consist of an identifier, a length indicator, and then a sequence of bytes equal in length
* to the length indicator.
*/
public enum TokenType
{
//Tokens common to request and response packets
IDENTITY("22", -1, "IDENTITY"),
UNKNOWN_23("23", 1, "UNKNOWN 23"),
//Request parameter tokens. Integer values are 7-bit variable length integers (varints).
TRIGGER_PERIODIC("34", 0, "PERIODIC", true), //Request meaning of 0x34. See TIMESTAMP for the response meaning.
TRIGGER_INTERVAL("31", -2, "TRIGGER INTERVAL"),
TRIGGER_CONDITION("4A", 1, "TRIGGER CONDITION"),
TRIGGER_DISTANCE("78", -2, "TRIGGER DISTANCE"),
REQUEST_PARAMETER_42("42", -2, "PARAM-42"), //Meaning unknown. Observed value: 3600
REQUEST_PARAMETER_44("44", -2, "PARAM-44"), //Meaning unknown. Observed value: 100
REQUEST_61("61", 1, "REQUEST-61"),
REQUEST_73("73", 1, "REQUEST-73"),
//Response tokens and single-byte requested field flags
TRIGGER_PERIODIC("31", 1, "TRIGGER PERIODIC"),
TIMESTAMP("34", 5, "TIMESTAMP"),
VERSION("36", 1, "VERSION"),
RESPONSE("37", -1, "RESPONSE"),
SUCCESS( "38", 0, "SUCCESS"),
REQUEST_3A("3A", 0,"REQUEST-3A"),
TRIGGER_GPIO("42", 0, "TRIGGER GPIO"),
TRIGGER_DISTANCE("4A", 1, "TRIGGER DISTANCE"), //Guess - not sure
ALTITUDE_ACCURACY("50", 0,"ALTITUDE ACCURACY"),
CIRCLE_2D("51", 10, "CIRCLE 2D"),
TIME("52", 0,"TIME"),
@@ -64,55 +48,37 @@ public enum TokenType
CIRCLE_3D("55", 15, "CIRCLE 3D"),
HEADING("56", 1, "HEADING"),
HORIZONTAL_DIRECTION("57", 0,"HORIZONTAL DIRECTION"),
HORIZONTAL_SPEED("62", 0,"HORIZONTAL SPEED"),
REQUEST_61("61", 1, "REQUEST-61"),
REQUEST_62("62", 0,"REQUEST-62"),
REQUEST_64("64", 0,"REQUEST-64"),
POINT_2D("66", 8, "POINT 2D"),
POINT_3D("69", 11, "POINT 3D"),
SPEED("6C", 2, "SPEED"),
REQUEST_73("73", 1, "REQUEST-73"),
TRIGGER_ON_MOVE("78", 1, "TRIGGER ON MOVE"),
REQUESTED_TOKENS("000", 0, "REQUESTED TOKENS"),
UNKNOWN("0",0, "UNKNOWN");
private static final int VARIABLE_LENGTH = -1;
private static final int VARINT_LENGTH = -2;
private String mValue;
private int mLength;
private String mLabel;
private boolean mRequestOnly;
/**
* Tokens that have parameters in request packet messages
*/
private static EnumSet<TokenType> REQUEST_PARAMETER_TOKENS = EnumSet.of(IDENTITY, TRIGGER_PERIODIC,
TRIGGER_INTERVAL, TRIGGER_CONDITION, TRIGGER_DISTANCE, REQUEST_PARAMETER_42, REQUEST_PARAMETER_44,
REQUEST_61, REQUEST_73);
private static EnumSet<TokenType> REQUEST_PARAMETER_TOKENS = EnumSet.of(IDENTITY, TRIGGER_DISTANCE, TRIGGER_GPIO,
TRIGGER_ON_MOVE, TRIGGER_PERIODIC, REQUEST_61, REQUEST_73);
/**
* Constructs an instance
* @param value of the token
* @param length of the token, exclusive of the identifier, where -1 indicates a variable length token and -2
* indicates a 7-bit variable length integer (varint) value.
* @param label for pretty printing
* @param length of the token, exclusive of the identifier, where -1 indicates a variable length token.
*/
TokenType(String value, int length, String label)
{
this(value, length, label, false);
}
/**
* Constructs an instance
* @param value of the token
* @param length of the token, exclusive of the identifier, where -1 indicates a variable length token and -2
* indicates a 7-bit variable length integer (varint) value.
* @param label for pretty printing
* @param requestOnly true when this meaning of the value applies to request packets only
*/
TokenType(String value, int length, String label, boolean requestOnly)
{
mValue = value;
mLength = length;
mLabel = label;
mRequestOnly = requestOnly;
}
/**
@@ -128,23 +94,7 @@ public enum TokenType
*/
public boolean isVariableLength()
{
return mLength == VARIABLE_LENGTH;
}
/**
* Indicates if this token's value is a 7-bit variable length integer (varint).
*/
public boolean isVarInt()
{
return mLength == VARINT_LENGTH;
}
/**
* Indicates if this meaning of the token value applies to request packets only.
*/
public boolean isRequestOnly()
{
return mRequestOnly;
return mLength == -1;
}
/**
@@ -166,26 +116,17 @@ public enum TokenType
private static final Map<String,TokenType> LOOKUP_MAP = new HashMap<>();
private static final Map<String,TokenType> REQUEST_LOOKUP_MAP = new HashMap<>();
static
{
for(TokenType tokenType: TokenType.values())
{
if(tokenType.isRequestOnly())
{
REQUEST_LOOKUP_MAP.put(tokenType.getValue(), tokenType);
}
else
{
LOOKUP_MAP.put(tokenType.getValue(), tokenType);
REQUEST_LOOKUP_MAP.putIfAbsent(tokenType.getValue(), tokenType);
}
LOOKUP_MAP.put(tokenType.getValue(), tokenType);
}
}
/**
* Lookup a token from a 2-character string value, using the response packet meaning of the value.
* Lookup a token from a 2-character string value
* @param value that represents the token
* @return matching token or UNKNOWN.
*/
@@ -199,21 +140,6 @@ public enum TokenType
return UNKNOWN;
}
/**
* Lookup a token from a 2-character string value, using the request packet meaning of the value.
* @param value that represents the token
* @return matching token or UNKNOWN.
*/
public static TokenType fromRequestValue(String value)
{
if(value != null && REQUEST_LOOKUP_MAP.containsKey(value))
{
return REQUEST_LOOKUP_MAP.get(value);
}
return UNKNOWN;
}
@Override
public String toString()
{

View File

@@ -1,135 +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.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP Trigger Condition Token
*
* Start Token: 0x4A
* Total Length: 2 bytes
*
* Event that causes the radio to send a triggered location report. Condition values are from node-dmr-lib
* (src/DMRConst.js) and match captured requests: 0x4A 0x02 emergency, 0x4A 0x42 pin status change (telemetry),
* 0x4A 0x50 PTT and 0x4A 0x51 PTT emergency.
*/
public class TriggerCondition extends Token
{
private static final int[] CONDITION = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**
* Trigger condition values
*/
public enum Condition
{
EMERGENCY(0x02, "EMERGENCY"),
PIN_STATUS_CHANGE(0x42, "PIN STATUS CHANGE"),
PTT(0x50, "PTT"),
PTT_EMERGENCY(0x51, "PTT EMERGENCY"),
UNKNOWN(-1, "UNKNOWN");
private int mValue;
private String mLabel;
Condition(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Numeric value of the condition
*/
public int getValue()
{
return mValue;
}
/**
* Lookup a condition from a value
* @param value to lookup
* @return matching condition or UNKNOWN
*/
public static Condition fromValue(int value)
{
for(Condition condition: Condition.values())
{
if(condition.getValue() == value)
{
return condition;
}
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}
/**
* Constructs an instance of a trigger condition token.
*
* @param message containing the token
* @param offset to the start of the token
*/
public TriggerCondition(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
@Override
public TokenType getTokenType()
{
return TokenType.TRIGGER_CONDITION;
}
/**
* Raw condition value
*/
public int getConditionValue()
{
return getMessage().getInt(CONDITION, getOffset());
}
/**
* Trigger condition
*/
public Condition getCondition()
{
return Condition.fromValue(getConditionValue());
}
@Override
public String toString()
{
Condition condition = getCondition();
if(condition == Condition.UNKNOWN)
{
return "TRIGGER CONDITION:UNKNOWN[0x" + String.format("%02X", getConditionValue()) + "]";
}
return "TRIGGER CONDITION:" + condition;
}
}

View File

@@ -24,23 +24,21 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP Trigger Distance Token
*
* Start Token: 0x78
* Total Length: 1 byte identifier plus a 7-bit variable length integer value (1-5 bytes)
*
* Distance in meters that the radio must move before it sends a triggered location report, e.g. 0x78 0x83 0x74 = 500.
* Total Length: 2 bytes
*/
public class TriggerDistance extends VarIntToken
public class TriggerDistance extends Token
{
private static final int[] DISTANCE = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**
* Constructs an instance of a trigger distance token.
* Constructs an instance of a heading token.
*
* @param message containing the token
* @param message containing the heading
* @param offset to the start of the token
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
*/
public TriggerDistance(CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
public TriggerDistance(CorrectedBinaryMessage message, int offset)
{
super(message, offset, remainingCharacterCount);
super(message, offset);
}
@Override
@@ -54,12 +52,12 @@ public class TriggerDistance extends VarIntToken
*/
public int getDistance()
{
return getValue();
return getMessage().getInt(DISTANCE, getOffset());
}
@Override
public String toString()
{
return "TRIGGER DISTANCE:" + getDistance() + " METERS";
return "TRIGGER DISTANCE:" + getDistance();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2023 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
@@ -16,49 +16,38 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP Trigger Interval Token
* LRRP Trigger on GPIO Token
*
* Start Token: 0x31
* Total Length: 1 byte identifier plus a 7-bit variable length integer value (1-5 bytes)
*
* Reporting interval in seconds for a periodic triggered location request, e.g. 0x31 0x84 0x58 = 600 seconds.
* Total Length: 1 byte
*/
public class TriggerInterval extends VarIntToken
public class TriggerGpio extends Token
{
/**
* Constructs an instance of a trigger interval token.
* Constructs an instance of a heading token.
*
* @param message containing the token
* @param message containing the heading
* @param offset to the start of the token
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
*/
public TriggerInterval(CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
public TriggerGpio(CorrectedBinaryMessage message, int offset)
{
super(message, offset, remainingCharacterCount);
super(message, offset);
}
@Override
public TokenType getTokenType()
{
return TokenType.TRIGGER_INTERVAL;
}
/**
* Interval in seconds
*/
public int getSeconds()
{
return getValue();
return TokenType.TRIGGER_GPIO;
}
@Override
public String toString()
{
return "INTERVAL:" + getSeconds() + " SECS";
return "TRIGGER ON GPIO";
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2023 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
@@ -17,19 +17,37 @@
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* 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
* LRRP Trigger on Move Token
*
* Total Length: 1 byte
*/
public record LimeSdrDeviceInformation(String serial, String board, String firmware, String hardware, String gateware,
String connection, String library)
public class TriggerOnMove extends Token
{
/**
* Constructs an instance of a heading token.
*
* @param message containing the heading
* @param offset to the start of the token
*/
public TriggerOnMove(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
@Override
public TokenType getTokenType()
{
return TokenType.TRIGGER_ON_MOVE;
}
@Override
public String toString()
{
return "TRIGGER ON MOVE";
}
}

View File

@@ -22,20 +22,18 @@ package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP Periodic Trigger Token (request packets)
* LRRP Trigger Periodic Token
*
* Start Token: 0x34 (in a response packet 0x34 is the Timestamp token)
* Total Length: 1 byte
*
* Flags a triggered location request as periodic. The reporting interval, when present, follows in a separate
* Trigger Interval (0x31) token, and a Trigger Condition (0x4A) or Trigger Distance (0x78) token can follow instead.
* Total Length: 2 bytes
*/
public class TriggerPeriodic extends Token
{
private static final int[] SECONDS = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**
* Constructs an instance of a periodic trigger token.
* Constructs an instance of a heading token.
*
* @param message containing the token
* @param message containing the heading
* @param offset to the start of the token
*/
public TriggerPeriodic(CorrectedBinaryMessage message, int offset)
@@ -46,12 +44,20 @@ public class TriggerPeriodic extends Token
@Override
public TokenType getTokenType()
{
return TokenType.TRIGGER_PERIODIC;
return TokenType.TRIGGER_DISTANCE;
}
/**
* Seconds for trigger
*/
public int getSeconds()
{
return getMessage().getInt(SECONDS, getOffset());
}
@Override
public String toString()
{
return "PERIODIC";
return "TRIGGER EVERY:" + getSeconds() + " SECS";
}
}

View File

@@ -1,105 +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.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.BinaryMessage;
/**
* LRRP variable length integer (varint) reader.
*
* LRRP encodes integer token values as one or more bytes where each byte carries seven value bits, most significant
* group first, and the high bit of a byte indicates that another byte follows. For example 0x84 0x58 decodes to
* (0x04 << 7) | 0x58 = 600, 0x9C 0x10 decodes to (0x1C << 7) | 0x10 = 3600 and a value smaller than 128 occupies a
* single byte (0x64 = 100).
*
* References:
* - Moto.Net, LRRPPacket.cs: "looks like this stuff is using VLQ ... for most of the fields"
* - node-dmr-lib, src/Protocols/LRRP.js: parseLRRPInt() and toLRRPInt()
*/
public final class VarInt
{
/**
* Maximum number of bytes consumed for a single value. Five 7-bit groups hold 35 bits, which covers any 32-bit
* value, and the cap keeps a corrupted run of continuation bits from consuming the rest of a packet.
*/
public static final int MAX_BYTES = 5;
private static final int[] BYTE_VALUE = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int CONTINUATION_MASK = 0x80;
private static final int VALUE_MASK = 0x7F;
private VarInt()
{
//Static utility class
}
/**
* Number of bytes occupied by the varint that starts at the bit offset.
*
* @param message containing the value
* @param offset bit offset to the first byte of the value
* @param limit maximum number of bytes available for the value (ie bytes remaining in the packet payload)
* @return byte count from 0 to the lesser of limit and MAX_BYTES. Zero indicates that no value is present.
*/
public static int length(BinaryMessage message, int offset, int limit)
{
int maximum = Math.min(limit, MAX_BYTES);
int count = 0;
while(count < maximum)
{
int b = message.getInt(BYTE_VALUE, offset + (count * 8));
count++;
if((b & CONTINUATION_MASK) == 0)
{
break;
}
}
return count;
}
/**
* Decodes the varint that starts at the bit offset.
*
* @param message containing the value
* @param offset bit offset to the first byte of the value
* @param limit maximum number of bytes available for the value (ie bytes remaining in the packet payload)
* @return decoded value, or zero when no bytes are available.
*/
public static int value(BinaryMessage message, int offset, int limit)
{
int maximum = Math.min(limit, MAX_BYTES);
int value = 0;
for(int count = 0; count < maximum; count++)
{
int b = message.getInt(BYTE_VALUE, offset + (count * 8));
value = (value << 7) | (b & VALUE_MASK);
if((b & CONTINUATION_MASK) == 0)
{
break;
}
}
return value;
}
}

View File

@@ -1,88 +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.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Base class for LRRP tokens whose value is a 7-bit variable length integer (see VarInt).
*
* The value starts in the byte that follows the token identifier and occupies one or more bytes. The read is
* bounded by the number of payload bytes that remain, so a token at the end of the payload cannot consume bytes
* that belong to the next packet.
*/
public abstract class VarIntToken extends Token
{
private static final int VALUE_START = 8;
private int mRemainingCharacterCount;
/**
* Constructs an instance
*
* @param message containing the token and value
* @param offset to the start of the token identifier
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
*/
public VarIntToken(CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
{
super(message, offset);
mRemainingCharacterCount = remainingCharacterCount;
}
/**
* Maximum number of value bytes that can follow the token identifier.
*/
private int getValueLimit()
{
return Math.max(0, mRemainingCharacterCount - 1);
}
/**
* Number of bytes occupied by the value, exclusive of the token identifier.
*/
public int getValueLength()
{
return VarInt.length(getMessage(), getOffset() + VALUE_START, getValueLimit());
}
/**
* Indicates if a value follows the token identifier.
*/
public boolean hasValue()
{
return getValueLength() > 0;
}
/**
* Decoded integer value, or zero when the token has no value.
*/
public int getValue()
{
return VarInt.value(getMessage(), getOffset() + VALUE_START, getValueLimit());
}
/**
* Total token length: identifier plus the variable length value.
*/
@Override
public int getByteLength()
{
return 1 + getValueLength();
}
}

View File

@@ -101,6 +101,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCStatusUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCTelephoneInterconnectChannelGrant;
@@ -192,14 +193,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
private final P25TrafficChannelManager mTrafficChannelManager;
private ServiceOptions mCurrentServiceOptions;
/**
* Encryption state of the current call as signalled by the algorithm ID in the HDU header or LDU2 encryption sync
* parameters (null when not yet known for this call). Some radios set the encryption flag in the service options of
* the LDU1 voice channel user link control while transmitting clear voice (HDU and LDU2 report ALGID 0x80), so the
* algorithm ID takes precedence over the link control service options when it is known.
*/
private Boolean mCallEncryptedByAlgorithm = null;
/**
* Constructs an APCO-25 decoder state with an optional traffic channel manager.
* @param channel with configuration details
@@ -427,12 +420,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(lcw instanceof IServiceOptionsProvider sop)
{
serviceOptions = sop.getServiceOptions();
//Trust the HDU/LDU2 algorithm ID over the link control service options encryption flag when known.
if(serviceOptions != null && serviceOptions.isEncrypted() && Boolean.FALSE.equals(mCallEncryptedByAlgorithm))
{
serviceOptions = VoiceServiceOptions.createUnencrypted(serviceOptions);
}
}
else if(lcw.isEncrypted())
{
@@ -612,7 +599,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
//Network configuration messages
case OSP_ADJACENT_STATUS_BROADCAST:
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
mNetworkConfigurationMonitor.process(ambtc);
break;
case OSP_NETWORK_STATUS_BROADCAST:
@@ -692,6 +678,14 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
mu.getShortDataMessage());
}
break;
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
if(ambtc instanceof AMBTCProtectionParameterBroadcast ppb)
{
broadcastEvent(ambtc, DecodeEventType.RESPONSE, "USE ENCRYPTION " + ppb.getEncryptionKey() +
" OUTBOUND MI:" + ppb.getOutboundMessageIndicator() +
" INBOUND MI:" + ppb.getInboundMessageIndicator());
}
break;
case OSP_ROAMING_ADDRESS_UPDATE:
broadcastEvent(ambtc.getIdentifiers(), ambtc.getTimestamp(), DecodeEventType.RESPONSE,
"ROAMING ADDRESS UPDATE");
@@ -852,7 +846,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
Identifier<?> radio = getIdentifierCollection().getFromIdentifier();
mCallEncryptedByAlgorithm = headerData.isEncryptedAudio();
mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio,
headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp());
@@ -895,7 +888,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(esp != null && esp.isValid())
{
getIdentifierCollection().update(esp.getIdentifiers());
mCallEncryptedByAlgorithm = esp.isEncryptedAudio();
if(esp.isEncryptedAudio())
{
@@ -919,7 +911,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
*/
private void processTDU(P25P1Message message)
{
mCallEncryptedByAlgorithm = null;
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message);
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
}
@@ -938,7 +929,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(lcw != null && lcw.isValid())
{
mCallEncryptedByAlgorithm = null;
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message);
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
processLC(lcw, message.getTimestamp(), true);

View File

@@ -35,7 +35,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSeconda
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSystemServiceBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.AMBTCMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcastUncoordinated;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
@@ -199,7 +198,6 @@ public class P25P1NetworkConfigurationMonitor
switch(ambtc.getHeader().getOpcode())
{
case OSP_ADJACENT_STATUS_BROADCAST:
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
if(ambtc instanceof AMBTCAdjacentStatusBroadcast)
{
AMBTCAdjacentStatusBroadcast aasb = (AMBTCAdjacentStatusBroadcast)ambtc;
@@ -504,24 +502,14 @@ public class P25P1NetworkConfigurationMonitor
if(mAMBTCNeighborSites.containsKey(site))
{
AMBTCAdjacentStatusBroadcast ambtc = mAMBTCNeighborSites.get(site);
sb.append(" ");
if(ambtc instanceof AMBTCAdjacentStatusBroadcastUncoordinated unc)
{
sb.append("WACN:").append(unc.getWacn()).append(" ");
}
sb.append("SYSTEM:").append(format(ambtc.getSystem(), 3));
sb.append(" SYSTEM:").append(format(ambtc.getSystem(), 3));
sb.append(" NAC:").append(format(ambtc.getNAC(), 3));
sb.append(" RFSS:").append(format(ambtc.getRfss(), 2));
sb.append(" SITE:").append(format(ambtc.getSite(), 2));
sb.append(" LRA:").append(format(ambtc.getLocationRegistrationArea(), 2));
sb.append(" CHANNEL:").append(ambtc.getChannel());
sb.append(" DOWNLINK:").append(ambtc.getChannel().getDownlinkFrequency());
sb.append(" UPLINK:").append(ambtc.getChannel().getUplinkFrequency());
if(ambtc instanceof AMBTCAdjacentStatusBroadcastUncoordinated unc)
{
sb.append(" SERVICES:").append(unc.getSystemServiceClass());
}
sb.append("\n");
sb.append(" UPLINK:").append(ambtc.getChannel().getUplinkFrequency()).append("\n");
}
if(mLCNeighborSites.containsKey(site))
{

View File

@@ -34,7 +34,7 @@ public class HDUMessage extends P25P1Message
private final static Logger mLog = LoggerFactory.getLogger(HDUMessage.class);
private static final int[] GOLAY_WORD_STARTS = {0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252,
270, 288, 306, 324, 342, 360, 378, 396, 414, 432, 450, 468, 486, 504, 522, 540, 558, 576, 594, 612, 630};
270, 288, 306, 324, 342, 360, 278, 396, 414, 432, 450, 468, 486, 504, 522, 540, 558, 576, 594, 612, 630};
private static final int[] CW_HEX_0 = {0, 1, 2, 3, 4, 5};
private static final int[] CW_HEX_1 = {18, 19, 20, 21, 22, 23};

View File

@@ -24,7 +24,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.AMBTCHeader
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketHeader;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.response.ResponseHeader;
import io.github.dsheirer.module.decode.p25.reference.PDUFormat;
import io.github.dsheirer.module.decode.p25.reference.ServiceAccessPoint;
public class PDUHeaderFactory
{
@@ -42,41 +41,8 @@ public class PDUHeaderFactory
case RESPONSE_PACKET_HEADER_FORMAT:
return new ResponseHeader(correctedBinaryMessage, passesCRC);
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
//Format 0x15 is the generic unconfirmed data header. Unconfirmed multi-block trunking control is
//only the trunking control SAP (61/63) use of it. With a packet data SAP the header has the same
//field layout as the confirmed packet data header and the data blocks carry an (IP) packet.
if(isPacketData(getServiceAccessPoint(correctedBinaryMessage)))
{
return new PacketHeader(correctedBinaryMessage, passesCRC);
}
return new PDUHeader(correctedBinaryMessage, passesCRC);
default:
return new PDUHeader(correctedBinaryMessage, passesCRC);
}
}
/**
* Service Access Point (SAP) from a packet data or unconfirmed data header message.
*/
public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage correctedBinaryMessage)
{
return ServiceAccessPoint.fromValue(correctedBinaryMessage.getInt(PacketHeader.SAP_ID));
}
/**
* Indicates if the SAP identifies packet data that is processed by the PDUMessageFactory as a packet message.
*/
public static boolean isPacketData(ServiceAccessPoint sap)
{
switch(sap)
{
case UNENCRYPTED_USER_DATA:
case PACKET_DATA:
case SNDCP_PACKET_DATA_CONTROL:
return true;
default:
return false;
}
}
}

View File

@@ -37,7 +37,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitAnswerResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitVoiceServiceRequest;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcastUncoordinated;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCCallAlert;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCFrequencyBandUpdateTDMA;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCGroupAffiliationQuery;
@@ -48,6 +47,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressUpdate;
@@ -173,13 +173,6 @@ public class PDUMessageFactory
case RESPONSE_PACKET_HEADER_FORMAT:
return new ResponseMessage(pduSequence, nac, timestamp);
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
//The header factory creates a packet header when the unconfirmed data header carries a packet data
//SAP, so that unconfirmed IP packets are processed the same as confirmed (PACKET_DATA) packets.
if(pduSequence.getHeader() instanceof PacketHeader)
{
return createPacketData(pduSequence, nac, timestamp);
}
return createUMBTC(pduSequence, nac, timestamp);
default:
return new PDUSequenceMessage(pduSequence, nac, timestamp);
@@ -236,7 +229,7 @@ public class PDUMessageFactory
}
/**
* Creates a packet data message from a confirmed or unconfirmed packet data sequence.
* Creates a packet data message
*
* @param pduSequence containing a packet data header and zero or more data blocks
* @param nac network access code
@@ -344,7 +337,7 @@ public class PDUMessageFactory
case OSP_NETWORK_STATUS_BROADCAST:
return new AMBTCNetworkStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
return new AMBTCAdjacentStatusBroadcastUncoordinated(pduSequence, nac, timestamp);
return new AMBTCProtectionParameterBroadcast(pduSequence, nac, timestamp);
case OSP_RFSS_STATUS_BROADCAST:
return new AMBTCRFSSStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ROAMING_ADDRESS_UPDATE:

View File

@@ -1,146 +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.p25.phase1.message.pdu.ambtc.osp;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.APCO25Wacn;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25ExplicitChannel;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.block.UnconfirmedDataBlock;
import io.github.dsheirer.module.decode.p25.reference.SystemServiceClass;
/**
* Adjacent Status Broadcast - Uncoordinated Band Plan (opcode 0x3E), extended/multi-block (AMBTC) format.
*
* Announces a neighbor site that may belong to a different WACN/system (and band plan) than the current site.
* Opcode 0x3E was previously defined as Protection Parameter Broadcast; current TIA-102.AABC revisions reassign it.
*
* Header: LRA (24-31), flags (32-35), SYSTEM (36-47), RFSS (64-71), SITE (72-79) -- same as opcode 0x3C
* Block 0: TRANSMIT CHANNEL (0-15), RECEIVE CHANNEL (16-31), SYSTEM SERVICE CLASS (32-39), WACN (40-59)
*
* A receive channel of 0xFFFF indicates that no separate receive channel is specified.
*/
public class AMBTCAdjacentStatusBroadcastUncoordinated extends AMBTCAdjacentStatusBroadcast
{
private static final int[] BLOCK_0_DOWNLINK_FREQUENCY_BAND = {0, 1, 2, 3};
private static final int[] BLOCK_0_DOWNLINK_CHANNEL_NUMBER = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] BLOCK_0_UPLINK_CHANNEL = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_UPLINK_FREQUENCY_BAND = {16, 17, 18, 19};
private static final int[] BLOCK_0_UPLINK_CHANNEL_NUMBER = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_SYSTEM_SERVICE_CLASS = {32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] BLOCK_0_WACN = {40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59};
private static final int UNSPECIFIED_CHANNEL = 0xFFFF;
private IBandChannelDescriptor mChannel;
private Identifier mWacn;
private SystemServiceClass mSystemServiceClass;
public AMBTCAdjacentStatusBroadcastUncoordinated(PDUSequence PDUSequence, int nac, long timestamp)
{
super(PDUSequence, nac, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" WACN:").append(getWacn());
sb.append(" SYSTEM:").append(getSystem());
sb.append(" LRA:").append(getLocationRegistrationArea());
sb.append(" RFSS:").append(getRfss());
sb.append(" SITE:").append(getSite());
sb.append(" CHANNEL:").append(getChannel());
sb.append(" SERVICES:").append(getSystemServiceClass());
return sb.toString();
}
/**
* WACN of the neighbor site
*/
public Identifier getWacn()
{
if(mWacn == null && hasDataBlock(0))
{
mWacn = APCO25Wacn.create(getDataBlock(0).getMessage().getInt(BLOCK_0_WACN));
}
return mWacn;
}
/**
* Services offered by the neighbor site
*/
public SystemServiceClass getSystemServiceClass()
{
if(mSystemServiceClass == null)
{
mSystemServiceClass = new SystemServiceClass(hasDataBlock(0) ?
getDataBlock(0).getMessage().getInt(BLOCK_0_SYSTEM_SERVICE_CLASS) : 0);
}
return mSystemServiceClass;
}
private boolean hasUplinkChannel()
{
return hasDataBlock(0) && getDataBlock(0).getMessage().getInt(BLOCK_0_UPLINK_CHANNEL) != UNSPECIFIED_CHANNEL;
}
@Override
public boolean isExtendedChannel()
{
return hasUplinkChannel() && super.isExtendedChannel();
}
@Override
public IBandChannelDescriptor getChannel()
{
if(mChannel == null)
{
if(hasDataBlock(0))
{
UnconfirmedDataBlock block = getDataBlock(0);
if(isExtendedChannel())
{
mChannel = APCO25ExplicitChannel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER),
block.getMessage().getInt(BLOCK_0_UPLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_UPLINK_CHANNEL_NUMBER));
}
else
{
mChannel = APCO25Channel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER));
}
}
else
{
mChannel = APCO25Channel.create(-1, 0);
}
}
return mChannel;
}
}

View File

@@ -80,13 +80,4 @@ public class VoiceServiceOptions extends ServiceOptions
{
return new VoiceServiceOptions(4);
}
/**
* Creates a copy of the supplied service options with the encryption flag cleared and all other options unchanged.
* @param serviceOptions to copy
*/
public static VoiceServiceOptions createUnencrypted(ServiceOptions serviceOptions)
{
return new VoiceServiceOptions(serviceOptions.mServiceOptions & ~ENCRYPTION_FLAG);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -247,7 +247,6 @@ public class DiagnosticMonitor
Attributes atts = findManifestAttributes();
if (atts != null) {
sb.append("\nVersion : " + atts.getValue("Implementation-Version"));
sb.append("\nGit Describe : " + atts.getValue("Git-Describe"));
sb.append("\nGradle Version : " + atts.getValue("Created-By"));
sb.append("\nBuild Timestamp : " + atts.getValue("Build-Timestamp"));
sb.append("\nBuild-JDK : " + atts.getValue("Build-JDK"));

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -48,7 +48,6 @@ public class SystemProperties
private static String PROPERTIES_FILENAME = "SDRTrunk.properties";
private static String MANIFEST_VERSION = "Implementation-Version";
private static String BUILD_TIMESTAMP = "Build-Timestamp";
private static String GIT_DESCRIBE = "Git-Describe";
private static SystemProperties INSTANCE;
private static Properties mProperties;
@@ -218,18 +217,12 @@ public class SystemProperties
{
String version = manifest.getMainAttributes().getValue(MANIFEST_VERSION);
String timestamp = manifest.getMainAttributes().getValue(BUILD_TIMESTAMP);
String gitDescribe = manifest.getMainAttributes().getValue(GIT_DESCRIBE);
if(version != null)
{
if(version.contains("nightly") && timestamp != null)
{
sb.append( " nightly - ").append(timestamp);
if(gitDescribe != null && !gitDescribe.equals("unknown"))
{
sb.append(" (").append(gitDescribe).append(")");
}
}
else
{

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,7 +32,6 @@ public class AirspyTunerConfiguration extends TunerConfiguration
private int mLNAGain = AirspyTunerController.LNA_GAIN_DEFAULT;
private boolean mMixerAGC = false;
private boolean mLNAAGC = false;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -130,15 +129,4 @@ public class AirspyTunerConfiguration extends TunerConfiguration
{
mLNAAGC = enabled;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

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).
@@ -177,22 +183,13 @@ public class AirspyTunerController extends TunerController
}
/**
* Turns off the bias-T and closes the device, which also stops streaming.
* Closes the device, which also stops streaming.
* @param device to close, or null
*/
private static void closeDevice(AirspyDevice device)
{
if(device != null)
{
try
{
device.setBiasTee(false);
}
catch(AirspyException | RuntimeException e)
{
mLog.warn("Error turning off Airspy bias-T before closing the device - " + e.getMessage());
}
try
{
device.close();
@@ -206,17 +203,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)
{
@@ -384,7 +462,6 @@ public class AirspyTunerController extends TunerController
setLNAGain(config.getLNAGain());
setMixerAGC(config.isMixerAGC());
setLNAAGC(config.isLNAAGC());
setBiasT(config.isBiasT());
//Set the gain mode last, so custom values are already set, and linearity and sensitivity modes will
//automatically override the custom values.
@@ -543,17 +620,6 @@ public class AirspyTunerController extends TunerController
execute(device -> device.setLnaAgc(enabled));
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
execute(device -> device.setBiasTee(enabled));
}
public void setGain(Gain gain) throws SourceException
{
if(gain != Gain.CUSTOM)

View File

@@ -66,7 +66,6 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
private JLabel mMixerGainValueLabel;
private JCheckBox mLNAAGCCheckBox;
private JCheckBox mMixerAGCCheckBox;
private JCheckBox mBiasTCheckBox;
/**
* Constructs an instance
@@ -117,8 +116,6 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getTunerInfoButton().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
getBiasTCheckBox().setSelected(hasTuner() && hasConfiguration() && getConfiguration().isBiasT());
updateGainComponents((hasTuner() && hasConfiguration()) ? getConfiguration().getGain() : null);
if(hasTuner())
@@ -162,9 +159,6 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel(""));
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Gain Control"), "wrap");
@@ -188,37 +182,6 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
add(getLNAGainValueLabel());
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
private JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(e ->
{
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setBiasT(mBiasTCheckBox.isSelected());
save();
}
catch(Exception e1)
{
mLog.error("Error setting Bias-T", e1);
JOptionPane.showMessageDialog(mBiasTCheckBox, "Couldn't set Bias-T: " + e1.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
private JCheckBox getLNAAGCCheckBox()
{
if(mLNAAGCCheckBox == null)
@@ -644,7 +607,6 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
getConfiguration().setLNAGain(getLNAGainSlider().getValue());
getConfiguration().setMixerAGC(getMixerAGCCheckBox().isSelected());
getConfiguration().setLNAAGC(getLNAAGCCheckBox().isSelected());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
saveConfiguration();
}
}

View File

@@ -32,7 +32,6 @@ public class AirspyHfTunerConfiguration extends TunerConfiguration
private boolean mAgc;
private boolean mLna;
private int mAttenuation;
private boolean mBiasT;
/**
* Default constructor for JAXB
@@ -115,25 +114,6 @@ public class AirspyHfTunerConfiguration extends TunerConfiguration
mLna = lna;
}
/**
* Indicates if the Bias-T power output is enabled. Only applies to devices whose firmware supports a bias-T.
* @return bias-T enabled state
*/
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
/**
* Sets the Bias-T power output enabled state
* @param enabled true to enable
*/
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
/**
* Attenuation setting.
* @return attenuation setting index value.

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
@@ -67,8 +73,6 @@ public class AirspyHfTunerController extends TunerController
private Attenuation mAttenuation = Attenuation.A0;
private boolean mAgcEnabled;
private boolean mLnaEnabled;
private boolean mBiasTSupported;
private boolean mBiasTEnabled;
private volatile float mSamplesPerMillisecond = DEFAULT_SAMPLE_RATE.getSampleRate() / 1000.0f;
private volatile int mBufferSampleCount = DEFAULT_BUFFER_SAMPLE_COUNT;
private boolean mStreaming = false;
@@ -112,8 +116,6 @@ public class AirspyHfTunerController extends TunerController
try
{
mBiasTSupported = false;
mBiasTEnabled = false;
mDevice = AirspyHf.open(serialNumber);
BoardInfo boardInfo = mDevice.boardInfo();
@@ -137,8 +139,6 @@ public class AirspyHfTunerController extends TunerController
}
mAvailableSampleRates = Collections.unmodifiableList(sampleRates);
mBiasTSupported = detectBiasTSupport(mDevice);
mBiasTEnabled = false;
//Force the sample rate to be applied, since the rate discovery leaves the device at the last rate.
mCurrentSampleRate = null;
@@ -161,7 +161,6 @@ public class AirspyHfTunerController extends TunerController
public void stop()
{
AirspyHfDevice device;
boolean biasTEnabled;
getLock().lock();
@@ -170,8 +169,6 @@ public class AirspyHfTunerController extends TunerController
device = mDevice;
mDevice = null;
mStreaming = false;
biasTEnabled = mBiasTEnabled;
mBiasTEnabled = false;
}
finally
{
@@ -179,58 +176,9 @@ public class AirspyHfTunerController extends TunerController
}
mNativeBufferBroadcaster.clear();
if(device != null && biasTEnabled)
{
try
{
device.setBiasTee(0);
}
catch(AirspyHfException | RuntimeException e)
{
mLog.warn("Error turning off Airspy HF bias-T before closing the device - " + e.getMessage());
}
}
closeDevice(device);
}
/**
* Detects if the device firmware supports a bias-T. Firmware without bias-T support rejects the bias-T commands.
* Some firmware (HF+ Discovery R5.0.1) rejects the bias-T count request, so this falls back to turning the bias-T
* off, which is a safe probe since the bias-T should be off at startup anyway.
* @param device to probe
* @return true if the device accepts bias-T commands
*/
private static boolean detectBiasTSupport(AirspyHfDevice device)
{
try
{
if(device.biasTeeCount() > 0)
{
device.setBiasTee(0);
return true;
}
return false;
}
catch(AirspyHfException | RuntimeException e)
{
//Bias-T count not supported - fall through and probe with a bias-T off command.
}
try
{
device.setBiasTee(0);
return true;
}
catch(AirspyHfException | RuntimeException e)
{
mLog.debug("Airspy HF firmware doesn't support bias-T - " + e.getMessage());
return false;
}
}
/**
* Closes the device, which also stops streaming.
* @param device to close, or null
@@ -252,13 +200,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
{
@@ -419,11 +448,6 @@ public class AirspyHfTunerController extends TunerController
setLna(airspyConfig.isLna());
setAttenuation(airspyConfig.getAttenuation());
setAgc(airspyConfig.isAgc());
if(isBiasTSupported())
{
setBiasT(airspyConfig.isBiasT());
}
}
catch(SourceException se)
{
@@ -646,38 +670,4 @@ public class AirspyHfTunerController extends TunerController
execute(device -> device.setAttenuationStep(attenuation.getValue()));
mAttenuation = attenuation;
}
/**
* Indicates if the device firmware supports a bias-T, available once the tuner is started.
* @return true if supported
*/
public boolean isBiasTSupported()
{
return mBiasTSupported;
}
/**
* Indicates if the Bias-T power output is enabled.
* @return enabled state
*/
public boolean getBiasT()
{
return mBiasTEnabled;
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException if the device doesn't support a bias-T or there is an error
*/
public void setBiasT(boolean enabled) throws SourceException
{
if(!mBiasTSupported)
{
throw new SourceException("Airspy HF firmware doesn't support bias-T");
}
execute(device -> device.setBiasTee(enabled ? 1 : 0));
mBiasTEnabled = enabled;
}
}

View File

@@ -46,7 +46,6 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
private JComboBox<Attenuation> mAttenuationCombo;
private JToggleButton mAgcToggleButton;
private JToggleButton mLnaToggleButton;
private JToggleButton mBiasTToggleButton;
/**
* Constructs an instance
@@ -101,7 +100,6 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
buttonPanel.setLayout(new MigLayout("insets 0", "[left]", ""));
buttonPanel.add(getAgcToggleButton());
buttonPanel.add(getLnaToggleButton());
buttonPanel.add(getBiasTToggleButton());
add(new JLabel(" "));
add(buttonPanel, "wrap");
@@ -127,12 +125,6 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getConfiguration().setAgc(getAgcToggleButton().isSelected());
getConfiguration().setLna(getLnaToggleButton().isSelected());
//Only update bias-T when the device supports it, so the saved setting isn't lost on unsupported firmware.
if(getTuner().getController().isBiasTSupported())
{
getConfiguration().setBiasT(getBiasTToggleButton().isSelected());
}
saveConfiguration();
}
}
@@ -166,11 +158,6 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getAgcToggleButton().setSelected(getTuner().getController().getAgc());
getLnaToggleButton().setEnabled(true);
getLnaToggleButton().setSelected(getTuner().getController().getLna());
boolean biasTSupported = getTuner().getController().isBiasTSupported();
getBiasTToggleButton().setEnabled(biasTSupported);
getBiasTToggleButton().setSelected(biasTSupported && getTuner().getController().getBiasT());
getBiasTToggleButton().setToolTipText(biasTSupported ? "Enable Bias-T power output for active antennas" :
"Bias-T not supported by this device or firmware");
getSampleRateCombo().setSelectedItem(getTuner().getController().getCurrentAirspySampleRate());
getAttenuationCombo().setEnabled(!getTuner().getController().getAgc());
getAttenuationCombo().setSelectedItem(getTuner().getController().getAttenuation());
@@ -182,9 +169,6 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getAgcToggleButton().setSelected(false);
getLnaToggleButton().setEnabled(false);
getLnaToggleButton().setSelected(false);
getBiasTToggleButton().setEnabled(false);
getBiasTToggleButton().setSelected(false);
getBiasTToggleButton().setToolTipText("No tuner available");
getSampleRateCombo().setEnabled(false);
getAttenuationCombo().setEnabled(false);
getAttenuationCombo().setSelectedItem(Attenuation.A0);
@@ -360,36 +344,4 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
return mLnaToggleButton;
}
/**
* Bias-T toggle button, enabled only when the device firmware supports a bias-T.
*/
private JToggleButton getBiasTToggleButton()
{
if(mBiasTToggleButton == null)
{
mBiasTToggleButton = new JToggleButton("Bias-T");
mBiasTToggleButton.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTToggleButton.setEnabled(false);
mBiasTToggleButton.addActionListener(e -> {
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setBiasT(mBiasTToggleButton.isSelected());
save();
}
catch(SourceException se)
{
mLog.error("Error setting Airspy HF Bias-T", se);
mBiasTToggleButton.setSelected(getTuner().getController().getBiasT());
JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
"Airspy HF Tuner Controller - couldn't change Bias-T setting " + se.getLocalizedMessage());
}
}
});
}
return mBiasTToggleButton;
}
}

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

@@ -35,7 +35,6 @@ public class FCD1TunerConfiguration extends TunerConfiguration
private LNAGain mLNAGain = LNAGain.LNA_GAIN_PLUS_20_0;
private LNAEnhance mLNAEnhance = LNAEnhance.LNA_ENHANCE_OFF;
private MixerGain mMixerGain = MixerGain.MIXER_GAIN_PLUS_12_0;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -133,15 +132,4 @@ public class FCD1TunerConfiguration extends TunerConfiguration
{
mGainCorrection = value;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -49,7 +49,6 @@ public class FCD1TunerController extends FCDTunerController
private LNAGain mLNAGain;
private LNAEnhance mLNAEnhance;
private MixerGain mMixerGain;
private boolean mBiasTEnabled = false;
/**
* Constructs an instance
@@ -124,61 +123,9 @@ public class FCD1TunerController extends FCDTunerController
//Re-cast any usb exceptions as source exceptions
throw new SourceException("FCD 1 Controller error applying tuner config - " + e.getLocalizedMessage());
}
//Apply the bias-T separately so that a bias-T error doesn't prevent the tuner from starting.
try
{
setBiasT(fcd1.isBiasT());
}
catch(SourceException se)
{
mLog.warn("FCD 1 Controller couldn't apply bias-T setting - " + se.getMessage());
}
}
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
try
{
send(FCDCommand.APP_SET_BIAS_TEE, enabled ? 1 : 0);
mBiasTEnabled = enabled;
}
catch(Exception e)
{
throw new SourceException("Couldn't set FCD Pro bias-T", e);
}
}
/**
* Turns off the bias-T, when enabled, before closing the device.
*/
@Override
public void stop()
{
if(mBiasTEnabled)
{
try
{
setBiasT(false);
}
catch(Exception e)
{
mLog.warn("Error turning off FCD Pro bias-T before closing the device - " + e.getMessage());
}
mBiasTEnabled = false;
}
super.stop();
}
/**
* Gets the current LNA Gain value from the controller and stores it
*/

View File

@@ -19,7 +19,6 @@
package io.github.dsheirer.source.tuner.fcd.proV1;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.fcd.FCDTuner;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAGain;
@@ -33,7 +32,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
@@ -54,7 +52,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
private JComboBox<LNAGain> mLnaGainCombo;
private JComboBox<LNAEnhance> mLnaEnhanceCombo;
private JComboBox<MixerGain> mMixerGainCombo;
private JCheckBox mBiasTCheckBox;
private CorrectionSpinner mCorrectionDCI;
private CorrectionSpinner mCorrectionDCQ;
private CorrectionSpinner mGainCorrectionSpinner;
@@ -124,7 +121,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getMixerGainCombo().setSelectedItem(getConfiguration().getMixerGain());
getLnaEnhanceCombo().setSelectedItem(getConfiguration().getLNAEnhance());
getLnaGainCombo().setSelectedItem(getConfiguration().getLNAGain());
getBiasTCheckBox().setSelected(getConfiguration().isBiasT());
getDcCorrectionSpinnerI().setValue(getConfiguration().getInphaseDCCorrection());
getDcCorrectionSpinnerQ().setValue(getConfiguration().getQuadratureDCCorrection());
getPhaseCorrectionSpinner().setValue(getConfiguration().getGainCorrection());
@@ -134,7 +130,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getMixerGainCombo().setEnabled(hasTuner());
getLnaEnhanceCombo().setEnabled(hasTuner());
getLnaGainCombo().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
getDcCorrectionSpinnerI().setEnabled(hasTuner());
getDcCorrectionSpinnerQ().setEnabled(hasTuner());
getPhaseCorrectionSpinner().setEnabled(hasTuner());
@@ -169,8 +164,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
add(getLnaEnhanceCombo(), "wrap");
add(new JLabel("Mixer:"));
add(getMixerGainCombo(), "wrap");
add(new JLabel(""));
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Correction"), "span,align center");
@@ -216,37 +209,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
return mLnaGainCombo;
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
public JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(event ->
{
if(hasTuner() && !isLoading())
{
try
{
getController().setBiasT(mBiasTCheckBox.isSelected());
save();
}
catch(SourceException e)
{
mLog.error("Couldn't set bias-T for FCD Pro", e);
JOptionPane.showMessageDialog(FCD1TunerEditor.this, "Couldn't set Bias-T: " + e.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
public JComboBox getLnaEnhanceCombo()
{
if(mLnaEnhanceCombo == null)
@@ -424,7 +386,6 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getConfiguration().setLNAEnhance((LNAEnhance) mLnaEnhanceCombo.getSelectedItem());
getConfiguration().setLNAGain((LNAGain) mLnaGainCombo.getSelectedItem());
getConfiguration().setMixerGain((MixerGain) mMixerGainCombo.getSelectedItem());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue();
getConfiguration().setInphaseDCCorrection(dci);
double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue();

View File

@@ -27,7 +27,6 @@ public class FCD2TunerConfiguration extends TunerConfiguration
{
private boolean mGainLNA = true;
private boolean mGainMixer = true;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -70,15 +69,4 @@ public class FCD2TunerConfiguration extends TunerConfiguration
{
mGainMixer = gain;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -43,7 +43,6 @@ public class FCD2TunerController extends FCDTunerController
public static final int MINIMUM_TUNABLE_FREQUENCY_HZ = 150000;
public static final int MAXIMUM_TUNABLE_FREQUENCY_HZ = 2050000000;
public static final int SAMPLE_RATE = 192000;
private boolean mBiasTEnabled = false;
/**
* Constructs an instance
@@ -99,48 +98,6 @@ public class FCD2TunerController extends FCDTunerController
send(FCDCommand.APP_SET_MIXER_GAIN, enabled ? 1 : 0);
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
try
{
send(FCDCommand.APP_SET_BIAS_TEE, enabled ? 1 : 0);
mBiasTEnabled = enabled;
}
catch(Exception e)
{
throw new SourceException("Couldn't set FCD Pro Plus bias-T", e);
}
}
/**
* Turns off the bias-T, when enabled, before closing the device.
*/
@Override
public void stop()
{
if(mBiasTEnabled)
{
try
{
setBiasT(false);
}
catch(Exception e)
{
mLog.warn("Error turning off FCD Pro Plus bias-T before closing the device - " + e.getMessage());
}
mBiasTEnabled = false;
}
super.stop();
}
@Override
public void apply(TunerConfiguration config) throws SourceException
{
@@ -151,7 +108,6 @@ public class FCD2TunerController extends FCDTunerController
{
setLNAGain(fcd2.getGainLNA());
setMixerGain(fcd2.getGainMixer());
setBiasT(fcd2.isBiasT());
}
}

Some files were not shown because too many files have changed in this diff Show More