mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 17:17:29 -05:00
Compare commits
1 Commits
v1.0.0
...
signal-hou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c5727145 |
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@@ -14,9 +14,6 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- 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
|
- name: Set up JDK 27
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v6
|
||||||
@@ -37,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Build Info file
|
- name: Create Build Info file
|
||||||
run: |
|
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
|
- name: Add build artifacts to nightly release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v3
|
||||||
|
|||||||
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Publishes a release when a version tag is pushed: v0.6.2 for a final release,
|
# Publishes a release when a version tag is pushed. The tag has to match
|
||||||
# or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release. The build derives the
|
# projectVersion in gradle.properties with a leading 'v': v0.6.2 for a final
|
||||||
# version from the tag (see build.gradle).
|
# release, or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release.
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -22,6 +22,15 @@ jobs:
|
|||||||
# Full history and tags, to find the previous release tag for the release notes
|
# Full history and tags, to find the previous release tag for the release notes
|
||||||
fetch-depth: 0
|
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
|
# 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.
|
# starts from the previous final release, so it covers every alpha and beta in between.
|
||||||
- id: previous
|
- id: previous
|
||||||
@@ -45,17 +54,6 @@ jobs:
|
|||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6
|
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
|
- name: Build and test
|
||||||
run: ./gradlew clean build --console=plain
|
run: ./gradlew clean build --console=plain
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -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
|
Releases are built and published automatically by the [Release](.github/workflows/release.yml) GitHub workflow
|
||||||
whenever a version tag is pushed to the repository.
|
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
|
1. Update `projectVersion` in `gradle.properties` to the new version, following the naming convention described in
|
||||||
with a leading `v`, following the naming convention described in `gradle.properties` (e.g. `v0.6.2-alpha-1`,
|
that file (e.g. `0.6.2-alpha-1`, `0.6.2-beta-1` or `0.6.2`).
|
||||||
`v0.6.2-beta-1` or `v0.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 tag v0.6.2-beta-1
|
||||||
git push origin 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:
|
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).
|
* 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
|
* 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
|
the PRs merged since the previous version tag), and attaches the six zip
|
||||||
|
|||||||
42
build.gradle
42
build.gradle
@@ -52,44 +52,8 @@ plugins {
|
|||||||
id 'org.beryx.runtime' version '2.0.1'
|
id 'org.beryx.runtime' version '2.0.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
|
||||||
* 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
|
version = project.findProperty('projectVersion')
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Java 27 is required for this version of the Project Panama preview/incubator feature
|
//Java 27 is required for this version of the Project Panama preview/incubator feature
|
||||||
java {
|
java {
|
||||||
@@ -168,6 +132,7 @@ dependencies {
|
|||||||
implementation 'io.github.dsheirer:jlimesdr:25.1.0.2' //LimeSDR 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 'io.github.dsheirer:jrtl:1.4.0.3' //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:jsdrplay:3.15.3' //SDRplay RSP tuners - GitHub release, see settings.gradle
|
||||||
|
implementation 'io.github.dsheirer:jsignalhound:2026.10.05' //Signal Hound tuners - GitHub release, see settings.gradle
|
||||||
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
|
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-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 'org.apache.commons:commons-lang3:3.21.0'
|
||||||
@@ -238,7 +203,6 @@ jar {
|
|||||||
attributes (
|
attributes (
|
||||||
'Implementation-Title' : 'sdrtrunk project',
|
'Implementation-Title' : 'sdrtrunk project',
|
||||||
'Implementation-Version' : archiveVersion,
|
'Implementation-Version' : archiveVersion,
|
||||||
'Git-Describe' : gitDescribe ?: 'unknown',
|
|
||||||
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
||||||
'Build-Timestamp' : new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
|
'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']}",
|
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
|
||||||
|
|||||||
@@ -17,16 +17,15 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
|
|
||||||
# The project version is no longer defined here. It is derived from the most recent 'v<version>' git tag (see
|
# Version naming conventions:
|
||||||
# build.gradle) and can be overridden with -PprojectVersion=<version>.
|
# Alpha: maj.min.patch-alpha-build
|
||||||
|
# Beta: maj.min.patch-beta-build
|
||||||
|
# Release: maj.min.patch
|
||||||
#
|
#
|
||||||
# Version tag naming conventions:
|
# Example: 0.6.0-beta-5
|
||||||
# Alpha: vmaj.min.patch-alpha-build
|
|
||||||
# Beta: vmaj.min.patch-beta-build
|
|
||||||
# Release: vmaj.min.patch
|
|
||||||
#
|
#
|
||||||
# Example: v0.6.0-beta-5
|
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
|
||||||
#
|
|
||||||
# Note: this is so that the file names for the resultant release build products match the GitHub release asset
|
|
||||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||||
|
|
||||||
|
projectVersion=1.0.0-beta-1
|
||||||
@@ -75,6 +75,15 @@ dependencyResolutionManagement {
|
|||||||
content { includeModule 'io.github.dsheirer', 'jsdrplay' }
|
content { includeModule 'io.github.dsheirer', 'jsdrplay' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//jSignalHound is published as a GitHub release. The jar does not contain the Signal Hound API native libraries,
|
||||||
|
//which users install separately from the Signal Hound SDK
|
||||||
|
ivy {
|
||||||
|
url = 'https://github.com/DSheirer/jSignalHound/releases/download'
|
||||||
|
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
|
||||||
|
metadataSources { artifact() }
|
||||||
|
content { includeModule 'io.github.dsheirer', 'jsignalhound' }
|
||||||
|
}
|
||||||
|
|
||||||
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
|
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
|
||||||
ivy {
|
ivy {
|
||||||
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
|
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
|
||||||
|
|||||||
@@ -68,29 +68,6 @@ public class Golay24
|
|||||||
*/
|
*/
|
||||||
public static int checkAndCorrect(CorrectedBinaryMessage message, int startIndex)
|
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;
|
boolean parityError = message.cardinality() % 2 != 0;
|
||||||
|
|
||||||
int syndrome = getSyndrome(message, startIndex);
|
int syndrome = getSyndrome(message, startIndex);
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.DisableSpectrumWaterfallMenuItem;
|
||||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||||
import io.github.dsheirer.util.DuplicateLibraryDetector;
|
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import io.github.dsheirer.util.TimeStamp;
|
import io.github.dsheirer.util.TimeStamp;
|
||||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
@@ -155,17 +153,6 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||||
mApplicationLog.start();
|
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);
|
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||||
|
|
||||||
ThreadPool.logSettings();
|
ThreadPool.logSettings();
|
||||||
@@ -266,11 +253,6 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
{
|
{
|
||||||
updateTitle(tuner.getPreferredName());
|
updateTitle(tuner.getPreferredName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!duplicateLibraries.isEmpty())
|
|
||||||
{
|
|
||||||
showDuplicateLibrariesDialog(duplicateLibraries);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(calibrating && !GraphicsEnvironment.isHeadless())
|
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.
|
* Launch the application.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -133,7 +133,6 @@ public class ApplicationLog
|
|||||||
Attributes atts = findManifestAttributes();
|
Attributes atts = findManifestAttributes();
|
||||||
if (atts != null) {
|
if (atts != null) {
|
||||||
mLog.info("SDRTrunk Version : " + atts.getValue("Implementation-Version"));
|
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("Gradle Version : " + atts.getValue("Created-By"));
|
||||||
mLog.info("Build Timestamp : " + atts.getValue("Build-Timestamp"));
|
mLog.info("Build Timestamp : " + atts.getValue("Build-Timestamp"));
|
||||||
mLog.info("Build-JDK : " + atts.getValue("Build-JDK"));
|
mLog.info("Build-JDK : " + atts.getValue("Build-JDK"));
|
||||||
|
|||||||
@@ -123,23 +123,25 @@ public class LRRPPacket extends Packet
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
String message = getMessage().getSubMessage(getOffset(), getOffset() + ((characterCount + 2) * 8)).toHexString();
|
||||||
|
int x = 0;
|
||||||
|
|
||||||
RequestedTokens requestedTokens = new RequestedTokens();
|
RequestedTokens requestedTokens = new RequestedTokens();
|
||||||
|
|
||||||
while(characterCount > 0 && offset < getMessage().size())
|
while(characterCount > 0 && offset < getMessage().size())
|
||||||
{
|
{
|
||||||
String tokenId = getTokenIdentifier(offset);
|
String tokenId = getTokenIdentifier(offset);
|
||||||
TokenType tokenType = TokenType.fromRequestValue(tokenId);
|
TokenType tokenType = TokenType.fromValue(tokenId);
|
||||||
|
|
||||||
if(tokenType.isRequestParameterToken())
|
if(tokenType.isRequestParameterToken())
|
||||||
{
|
{
|
||||||
Token token = TokenFactory.createToken(tokenType, getMessage(), offset, characterCount);
|
Token token = TokenFactory.createToken(tokenId, getMessage(), offset, characterCount);
|
||||||
mTokens.add(token);
|
mTokens.add(token);
|
||||||
characterCount -= token.getByteLength();
|
characterCount -= token.getByteLength();
|
||||||
offset += (8 * token.getByteLength());
|
offset += (8 * token.getByteLength());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Single-byte flag naming a field that the radio should include in its location report
|
|
||||||
if(tokenType != TokenType.UNKNOWN)
|
if(tokenType != TokenType.UNKNOWN)
|
||||||
{
|
{
|
||||||
requestedTokens.add(tokenType);
|
requestedTokens.add(tokenType);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|||||||
* LRRP Identity Token
|
* LRRP Identity Token
|
||||||
*
|
*
|
||||||
* Start Token: 0x22
|
* Start Token: 0x22
|
||||||
* Total Length: 2 bytes plus a 1-4 byte request identifier
|
* Total Length: 6 bytes
|
||||||
*/
|
*/
|
||||||
public class Identity extends Token
|
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};
|
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
|
* @param offset to the start of the token
|
||||||
*/
|
*/
|
||||||
public Identity(CorrectedBinaryMessage message, int offset)
|
public Identity(CorrectedBinaryMessage message, int offset)
|
||||||
@@ -50,47 +50,47 @@ public class Identity extends Token
|
|||||||
return TokenType.IDENTITY;
|
return TokenType.IDENTITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private BinaryMessage getTokenContents()
|
||||||
* 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()
|
|
||||||
{
|
{
|
||||||
int start = getOffset() + IDENTIFIER_START;
|
return getMessage().getSubMessage(getOffset(), getOffset() + 48);
|
||||||
int available = Math.max(0, getMessage().size() - start);
|
|
||||||
int length = Math.min(getVariableLengthPayloadSize() * 8, available);
|
|
||||||
return getMessage().getSubMessage(start, start + length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request identifier assembled big-endian from the 1-4 payload bytes. Moto.Net (LRRPPacket.cs) and node-dmr-lib
|
* Extracts the payload contents.
|
||||||
* (LRRP.js) both read the identifier this way.
|
|
||||||
*
|
*
|
||||||
* @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()
|
public int getID()
|
||||||
{
|
{
|
||||||
int length = getVariableLengthPayloadSize();
|
int length = getVariableLengthPayloadSize();
|
||||||
|
int payloadOffset = 16;
|
||||||
if(length < 1 || length > 4)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int value = 0;
|
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;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
//Hexadecimal, as transmitted (e.g. ID:FFFFEE)
|
int id = getID();
|
||||||
return "ID:" + getIdentifierBytes().toHexString();
|
|
||||||
|
if(id != 0)
|
||||||
|
{
|
||||||
|
return "ID:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ID:" + getTokenContents().toHexString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,28 +63,12 @@ public class RequestedTokens extends Token
|
|||||||
Collections.sort(mUnrecognizedRequestedTokens);
|
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.
|
* Indicates if this object has requested tokens.
|
||||||
*/
|
*/
|
||||||
public boolean hasRequestedTokens()
|
public boolean hasRequestedTokens()
|
||||||
{
|
{
|
||||||
return !mRequestedTokens.isEmpty() || !mUnrecognizedRequestedTokens.isEmpty();
|
return !mRequestedTokens.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.TimeZone;
|
|||||||
/**
|
/**
|
||||||
* LRRP Timestamp Token
|
* 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
|
* Total Length: 6 bytes
|
||||||
*/
|
*/
|
||||||
public class Timestamp extends Token
|
public class Timestamp extends Token
|
||||||
|
|||||||
@@ -88,11 +88,6 @@ public abstract class Token
|
|||||||
{
|
{
|
||||||
return getVariableLengthPayloadSize() + 2; //Variable Length Payload + Id + Length Field
|
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
|
else
|
||||||
{
|
{
|
||||||
return getTokenType().getLength() + 1; //Fixed Length Payload + Id
|
return getTokenType().getLength() + 1; //Fixed Length Payload + Id
|
||||||
|
|||||||
@@ -26,32 +26,10 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|||||||
*/
|
*/
|
||||||
public class TokenFactory
|
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)
|
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)
|
switch(tokenType)
|
||||||
{
|
{
|
||||||
case HEADING:
|
case HEADING:
|
||||||
@@ -74,17 +52,14 @@ public class TokenFactory
|
|||||||
return new Success(message, offset);
|
return new Success(message, offset);
|
||||||
case TIMESTAMP:
|
case TIMESTAMP:
|
||||||
return new Timestamp(message, offset);
|
return new Timestamp(message, offset);
|
||||||
case TRIGGER_CONDITION:
|
|
||||||
return new TriggerCondition(message, offset);
|
|
||||||
case TRIGGER_DISTANCE:
|
case TRIGGER_DISTANCE:
|
||||||
return new TriggerDistance(message, offset, remainingCharacterCount);
|
return new TriggerDistance(message, offset);
|
||||||
case TRIGGER_INTERVAL:
|
case TRIGGER_GPIO:
|
||||||
return new TriggerInterval(message, offset, remainingCharacterCount);
|
return new TriggerGpio(message, offset);
|
||||||
|
case TRIGGER_ON_MOVE:
|
||||||
|
return new TriggerOnMove(message, offset);
|
||||||
case TRIGGER_PERIODIC:
|
case TRIGGER_PERIODIC:
|
||||||
return new TriggerPeriodic(message, offset);
|
return new TriggerPeriodic(message, offset);
|
||||||
case REQUEST_PARAMETER_42:
|
|
||||||
case REQUEST_PARAMETER_44:
|
|
||||||
return new RequestParameter(tokenType, message, offset, remainingCharacterCount);
|
|
||||||
case VERSION:
|
case VERSION:
|
||||||
return new Version(message, offset);
|
return new Version(message, offset);
|
||||||
case UNKNOWN_23:
|
case UNKNOWN_23:
|
||||||
|
|||||||
@@ -26,37 +26,21 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* LRRP Token Types
|
* LRRP Token Types
|
||||||
*
|
*
|
||||||
* Fixed length tokens consist of an identifier followed by a fixed number of value bytes. Variable length tokens
|
* Variable length tokens consist of an identifier, a length indicator, and then a sequence of bytes equal in length
|
||||||
* consist of an identifier, a length indicator, and then a sequence of bytes equal in length to the length indicator.
|
* 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.
|
|
||||||
*/
|
*/
|
||||||
public enum TokenType
|
public enum TokenType
|
||||||
{
|
{
|
||||||
//Tokens common to request and response packets
|
|
||||||
IDENTITY("22", -1, "IDENTITY"),
|
IDENTITY("22", -1, "IDENTITY"),
|
||||||
UNKNOWN_23("23", 1, "UNKNOWN 23"),
|
UNKNOWN_23("23", 1, "UNKNOWN 23"),
|
||||||
|
TRIGGER_PERIODIC("31", 1, "TRIGGER PERIODIC"),
|
||||||
//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
|
|
||||||
TIMESTAMP("34", 5, "TIMESTAMP"),
|
TIMESTAMP("34", 5, "TIMESTAMP"),
|
||||||
VERSION("36", 1, "VERSION"),
|
VERSION("36", 1, "VERSION"),
|
||||||
RESPONSE("37", -1, "RESPONSE"),
|
RESPONSE("37", -1, "RESPONSE"),
|
||||||
SUCCESS( "38", 0, "SUCCESS"),
|
SUCCESS( "38", 0, "SUCCESS"),
|
||||||
REQUEST_3A("3A", 0,"REQUEST-3A"),
|
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"),
|
ALTITUDE_ACCURACY("50", 0,"ALTITUDE ACCURACY"),
|
||||||
CIRCLE_2D("51", 10, "CIRCLE 2D"),
|
CIRCLE_2D("51", 10, "CIRCLE 2D"),
|
||||||
TIME("52", 0,"TIME"),
|
TIME("52", 0,"TIME"),
|
||||||
@@ -64,55 +48,37 @@ public enum TokenType
|
|||||||
CIRCLE_3D("55", 15, "CIRCLE 3D"),
|
CIRCLE_3D("55", 15, "CIRCLE 3D"),
|
||||||
HEADING("56", 1, "HEADING"),
|
HEADING("56", 1, "HEADING"),
|
||||||
HORIZONTAL_DIRECTION("57", 0,"HORIZONTAL DIRECTION"),
|
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"),
|
REQUEST_64("64", 0,"REQUEST-64"),
|
||||||
POINT_2D("66", 8, "POINT 2D"),
|
POINT_2D("66", 8, "POINT 2D"),
|
||||||
POINT_3D("69", 11, "POINT 3D"),
|
POINT_3D("69", 11, "POINT 3D"),
|
||||||
SPEED("6C", 2, "SPEED"),
|
SPEED("6C", 2, "SPEED"),
|
||||||
|
REQUEST_73("73", 1, "REQUEST-73"),
|
||||||
|
TRIGGER_ON_MOVE("78", 1, "TRIGGER ON MOVE"),
|
||||||
REQUESTED_TOKENS("000", 0, "REQUESTED TOKENS"),
|
REQUESTED_TOKENS("000", 0, "REQUESTED TOKENS"),
|
||||||
UNKNOWN("0",0, "UNKNOWN");
|
UNKNOWN("0",0, "UNKNOWN");
|
||||||
|
|
||||||
private static final int VARIABLE_LENGTH = -1;
|
|
||||||
private static final int VARINT_LENGTH = -2;
|
|
||||||
|
|
||||||
private String mValue;
|
private String mValue;
|
||||||
private int mLength;
|
private int mLength;
|
||||||
private String mLabel;
|
private String mLabel;
|
||||||
private boolean mRequestOnly;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tokens that have parameters in request packet messages
|
* Tokens that have parameters in request packet messages
|
||||||
*/
|
*/
|
||||||
private static EnumSet<TokenType> REQUEST_PARAMETER_TOKENS = EnumSet.of(IDENTITY, TRIGGER_PERIODIC,
|
private static EnumSet<TokenType> REQUEST_PARAMETER_TOKENS = EnumSet.of(IDENTITY, TRIGGER_DISTANCE, TRIGGER_GPIO,
|
||||||
TRIGGER_INTERVAL, TRIGGER_CONDITION, TRIGGER_DISTANCE, REQUEST_PARAMETER_42, REQUEST_PARAMETER_44,
|
TRIGGER_ON_MOVE, TRIGGER_PERIODIC, REQUEST_61, REQUEST_73);
|
||||||
REQUEST_61, REQUEST_73);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
* @param value of the token
|
* @param value of the token
|
||||||
* @param length of the token, exclusive of the identifier, where -1 indicates a variable length token and -2
|
* @param length of the token, exclusive of the identifier, where -1 indicates a variable length token.
|
||||||
* indicates a 7-bit variable length integer (varint) value.
|
|
||||||
* @param label for pretty printing
|
|
||||||
*/
|
*/
|
||||||
TokenType(String value, int length, String label)
|
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;
|
mValue = value;
|
||||||
mLength = length;
|
mLength = length;
|
||||||
mLabel = label;
|
mLabel = label;
|
||||||
mRequestOnly = requestOnly;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,23 +94,7 @@ public enum TokenType
|
|||||||
*/
|
*/
|
||||||
public boolean isVariableLength()
|
public boolean isVariableLength()
|
||||||
{
|
{
|
||||||
return mLength == VARIABLE_LENGTH;
|
return mLength == -1;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,26 +116,17 @@ public enum TokenType
|
|||||||
|
|
||||||
|
|
||||||
private static final Map<String,TokenType> LOOKUP_MAP = new HashMap<>();
|
private static final Map<String,TokenType> LOOKUP_MAP = new HashMap<>();
|
||||||
private static final Map<String,TokenType> REQUEST_LOOKUP_MAP = new HashMap<>();
|
|
||||||
|
|
||||||
static
|
static
|
||||||
{
|
{
|
||||||
for(TokenType tokenType: TokenType.values())
|
for(TokenType tokenType: TokenType.values())
|
||||||
{
|
{
|
||||||
if(tokenType.isRequestOnly())
|
LOOKUP_MAP.put(tokenType.getValue(), tokenType);
|
||||||
{
|
|
||||||
REQUEST_LOOKUP_MAP.put(tokenType.getValue(), tokenType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOOKUP_MAP.put(tokenType.getValue(), tokenType);
|
|
||||||
REQUEST_LOOKUP_MAP.putIfAbsent(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
|
* @param value that represents the token
|
||||||
* @return matching token or UNKNOWN.
|
* @return matching token or UNKNOWN.
|
||||||
*/
|
*/
|
||||||
@@ -199,21 +140,6 @@ public enum TokenType
|
|||||||
return UNKNOWN;
|
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
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,23 +24,21 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|||||||
/**
|
/**
|
||||||
* LRRP Trigger Distance Token
|
* LRRP Trigger Distance Token
|
||||||
*
|
*
|
||||||
* Start Token: 0x78
|
* Total Length: 2 bytes
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
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 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
|
@Override
|
||||||
@@ -54,12 +52,12 @@ public class TriggerDistance extends VarIntToken
|
|||||||
*/
|
*/
|
||||||
public int getDistance()
|
public int getDistance()
|
||||||
{
|
{
|
||||||
return getValue();
|
return getMessage().getInt(DISTANCE, getOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return "TRIGGER DISTANCE:" + getDistance() + " METERS";
|
return "TRIGGER DISTANCE:" + getDistance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
|
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
|
||||||
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LRRP Trigger Interval Token
|
* LRRP Trigger on GPIO Token
|
||||||
*
|
*
|
||||||
* Start Token: 0x31
|
* Total Length: 1 byte
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
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 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
|
@Override
|
||||||
public TokenType getTokenType()
|
public TokenType getTokenType()
|
||||||
{
|
{
|
||||||
return TokenType.TRIGGER_INTERVAL;
|
return TokenType.TRIGGER_GPIO;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interval in seconds
|
|
||||||
*/
|
|
||||||
public int getSeconds()
|
|
||||||
{
|
|
||||||
return getValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return "INTERVAL:" + getSeconds() + " SECS";
|
return "TRIGGER ON GPIO";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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
|
||||||
|
* 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 on Move Token
|
||||||
|
*
|
||||||
|
* Total Length: 1 byte
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,20 +22,18 @@ package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
|
|||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
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: 2 bytes
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
public class TriggerPeriodic extends Token
|
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
|
* @param offset to the start of the token
|
||||||
*/
|
*/
|
||||||
public TriggerPeriodic(CorrectedBinaryMessage message, int offset)
|
public TriggerPeriodic(CorrectedBinaryMessage message, int offset)
|
||||||
@@ -46,12 +44,20 @@ public class TriggerPeriodic extends Token
|
|||||||
@Override
|
@Override
|
||||||
public TokenType getTokenType()
|
public TokenType getTokenType()
|
||||||
{
|
{
|
||||||
return TokenType.TRIGGER_PERIODIC;
|
return TokenType.TRIGGER_DISTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seconds for trigger
|
||||||
|
*/
|
||||||
|
public int getSeconds()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(SECONDS, getOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return "PERIODIC";
|
return "TRIGGER EVERY:" + getSeconds() + " SECS";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -192,14 +192,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
private final P25TrafficChannelManager mTrafficChannelManager;
|
private final P25TrafficChannelManager mTrafficChannelManager;
|
||||||
private ServiceOptions mCurrentServiceOptions;
|
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.
|
* Constructs an APCO-25 decoder state with an optional traffic channel manager.
|
||||||
* @param channel with configuration details
|
* @param channel with configuration details
|
||||||
@@ -427,12 +419,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
if(lcw instanceof IServiceOptionsProvider sop)
|
if(lcw instanceof IServiceOptionsProvider sop)
|
||||||
{
|
{
|
||||||
serviceOptions = sop.getServiceOptions();
|
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())
|
else if(lcw.isEncrypted())
|
||||||
{
|
{
|
||||||
@@ -852,7 +838,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
|
|
||||||
Identifier<?> radio = getIdentifierCollection().getFromIdentifier();
|
Identifier<?> radio = getIdentifierCollection().getFromIdentifier();
|
||||||
|
|
||||||
mCallEncryptedByAlgorithm = headerData.isEncryptedAudio();
|
|
||||||
mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio,
|
mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio,
|
||||||
headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp());
|
headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp());
|
||||||
|
|
||||||
@@ -895,7 +880,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
if(esp != null && esp.isValid())
|
if(esp != null && esp.isValid())
|
||||||
{
|
{
|
||||||
getIdentifierCollection().update(esp.getIdentifiers());
|
getIdentifierCollection().update(esp.getIdentifiers());
|
||||||
mCallEncryptedByAlgorithm = esp.isEncryptedAudio();
|
|
||||||
|
|
||||||
if(esp.isEncryptedAudio())
|
if(esp.isEncryptedAudio())
|
||||||
{
|
{
|
||||||
@@ -919,7 +903,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
*/
|
*/
|
||||||
private void processTDU(P25P1Message message)
|
private void processTDU(P25P1Message message)
|
||||||
{
|
{
|
||||||
mCallEncryptedByAlgorithm = null;
|
|
||||||
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message);
|
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message);
|
||||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
||||||
}
|
}
|
||||||
@@ -938,7 +921,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
|
|
||||||
if(lcw != null && lcw.isValid())
|
if(lcw != null && lcw.isValid())
|
||||||
{
|
{
|
||||||
mCallEncryptedByAlgorithm = null;
|
|
||||||
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message);
|
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message);
|
||||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
||||||
processLC(lcw, message.getTimestamp(), true);
|
processLC(lcw, message.getTimestamp(), true);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class HDUMessage extends P25P1Message
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(HDUMessage.class);
|
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,
|
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_0 = {0, 1, 2, 3, 4, 5};
|
||||||
private static final int[] CW_HEX_1 = {18, 19, 20, 21, 22, 23};
|
private static final int[] CW_HEX_1 = {18, 19, 20, 21, 22, 23};
|
||||||
|
|||||||
@@ -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.packet.PacketHeader;
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.response.ResponseHeader;
|
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.PDUFormat;
|
||||||
import io.github.dsheirer.module.decode.p25.reference.ServiceAccessPoint;
|
|
||||||
|
|
||||||
public class PDUHeaderFactory
|
public class PDUHeaderFactory
|
||||||
{
|
{
|
||||||
@@ -42,41 +41,8 @@ public class PDUHeaderFactory
|
|||||||
case RESPONSE_PACKET_HEADER_FORMAT:
|
case RESPONSE_PACKET_HEADER_FORMAT:
|
||||||
return new ResponseHeader(correctedBinaryMessage, passesCRC);
|
return new ResponseHeader(correctedBinaryMessage, passesCRC);
|
||||||
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
|
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:
|
default:
|
||||||
return new PDUHeader(correctedBinaryMessage, passesCRC);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,13 +173,6 @@ public class PDUMessageFactory
|
|||||||
case RESPONSE_PACKET_HEADER_FORMAT:
|
case RESPONSE_PACKET_HEADER_FORMAT:
|
||||||
return new ResponseMessage(pduSequence, nac, timestamp);
|
return new ResponseMessage(pduSequence, nac, timestamp);
|
||||||
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
|
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);
|
return createUMBTC(pduSequence, nac, timestamp);
|
||||||
default:
|
default:
|
||||||
return new PDUSequenceMessage(pduSequence, nac, timestamp);
|
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 pduSequence containing a packet data header and zero or more data blocks
|
||||||
* @param nac network access code
|
* @param nac network access code
|
||||||
|
|||||||
@@ -80,13 +80,4 @@ public class VoiceServiceOptions extends ServiceOptions
|
|||||||
{
|
{
|
||||||
return new VoiceServiceOptions(4);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -247,7 +247,6 @@ public class DiagnosticMonitor
|
|||||||
Attributes atts = findManifestAttributes();
|
Attributes atts = findManifestAttributes();
|
||||||
if (atts != null) {
|
if (atts != null) {
|
||||||
sb.append("\nVersion : " + atts.getValue("Implementation-Version"));
|
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("\nGradle Version : " + atts.getValue("Created-By"));
|
||||||
sb.append("\nBuild Timestamp : " + atts.getValue("Build-Timestamp"));
|
sb.append("\nBuild Timestamp : " + atts.getValue("Build-Timestamp"));
|
||||||
sb.append("\nBuild-JDK : " + atts.getValue("Build-JDK"));
|
sb.append("\nBuild-JDK : " + atts.getValue("Build-JDK"));
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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 PROPERTIES_FILENAME = "SDRTrunk.properties";
|
||||||
private static String MANIFEST_VERSION = "Implementation-Version";
|
private static String MANIFEST_VERSION = "Implementation-Version";
|
||||||
private static String BUILD_TIMESTAMP = "Build-Timestamp";
|
private static String BUILD_TIMESTAMP = "Build-Timestamp";
|
||||||
private static String GIT_DESCRIBE = "Git-Describe";
|
|
||||||
|
|
||||||
private static SystemProperties INSTANCE;
|
private static SystemProperties INSTANCE;
|
||||||
private static Properties mProperties;
|
private static Properties mProperties;
|
||||||
@@ -218,18 +217,12 @@ public class SystemProperties
|
|||||||
{
|
{
|
||||||
String version = manifest.getMainAttributes().getValue(MANIFEST_VERSION);
|
String version = manifest.getMainAttributes().getValue(MANIFEST_VERSION);
|
||||||
String timestamp = manifest.getMainAttributes().getValue(BUILD_TIMESTAMP);
|
String timestamp = manifest.getMainAttributes().getValue(BUILD_TIMESTAMP);
|
||||||
String gitDescribe = manifest.getMainAttributes().getValue(GIT_DESCRIBE);
|
|
||||||
|
|
||||||
if(version != null)
|
if(version != null)
|
||||||
{
|
{
|
||||||
if(version.contains("nightly") && timestamp != null)
|
if(version.contains("nightly") && timestamp != null)
|
||||||
{
|
{
|
||||||
sb.append( " nightly - ").append(timestamp);
|
sb.append( " nightly - ").append(timestamp);
|
||||||
|
|
||||||
if(gitDescribe != null && !gitDescribe.equals("unknown"))
|
|
||||||
{
|
|
||||||
sb.append(" (").append(gitDescribe).append(")");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public enum TunerClass
|
|||||||
LIMESDR("LimeSDR"),
|
LIMESDR("LimeSDR"),
|
||||||
RTL2832("RTL-2832"),
|
RTL2832("RTL-2832"),
|
||||||
RSP("RSP"),
|
RSP("RSP"),
|
||||||
|
SIGNAL_HOUND("Signal Hound"),
|
||||||
TEST_TUNER("Test"),
|
TEST_TUNER("Test"),
|
||||||
RECORDING_TUNER("Recording"),
|
RECORDING_TUNER("Recording"),
|
||||||
UNKNOWN("Unknown" );
|
UNKNOWN("Unknown" );
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.preference.UserPreferences;
|
|||||||
import io.github.dsheirer.preference.source.ChannelizerType;
|
import io.github.dsheirer.preference.source.ChannelizerType;
|
||||||
import io.github.dsheirer.sdrplay.DeviceInfo;
|
import io.github.dsheirer.sdrplay.DeviceInfo;
|
||||||
import io.github.dsheirer.sdrplay.RspModel;
|
import io.github.dsheirer.sdrplay.RspModel;
|
||||||
|
import io.github.dsheirer.signalhound.DeviceDescriptor;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import io.github.dsheirer.source.mixer.MixerManager;
|
import io.github.dsheirer.source.mixer.MixerManager;
|
||||||
import io.github.dsheirer.source.tuner.airspy.AirspyTuner;
|
import io.github.dsheirer.source.tuner.airspy.AirspyTuner;
|
||||||
@@ -121,6 +122,11 @@ import io.github.dsheirer.source.tuner.sdrplay.rspDx.IControlRspDx;
|
|||||||
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerConfiguration;
|
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerController;
|
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerController;
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerEditor;
|
import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerEditor;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.DiscoveredSignalHoundTuner;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.SignalHoundTuner;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.SignalHoundTunerConfiguration;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.SignalHoundTunerController;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.SignalHoundTunerEditor;
|
||||||
import io.github.dsheirer.source.tuner.ui.TunerEditor;
|
import io.github.dsheirer.source.tuner.ui.TunerEditor;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -276,6 +282,20 @@ public class TunerFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Signal Hound tuner
|
||||||
|
* @param descriptor of the device from the Signal Hound API device list
|
||||||
|
* @param channelizerType for the tuner
|
||||||
|
* @param tunerErrorListener to listen for errors from the tuner
|
||||||
|
* @return tuner
|
||||||
|
*/
|
||||||
|
public static SignalHoundTuner getSignalHoundTuner(DeviceDescriptor descriptor, ChannelizerType channelizerType,
|
||||||
|
ITunerErrorListener tunerErrorListener)
|
||||||
|
{
|
||||||
|
return new SignalHoundTuner(new SignalHoundTunerController(descriptor, tunerErrorListener), tunerErrorListener,
|
||||||
|
channelizerType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a USB tuner
|
* Create a USB tuner
|
||||||
* @param tunerClass to instantiate
|
* @param tunerClass to instantiate
|
||||||
@@ -380,6 +400,8 @@ public class TunerFactory
|
|||||||
return new RspDuoTuner2Configuration(uniqueID);
|
return new RspDuoTuner2Configuration(uniqueID);
|
||||||
case RSP_DX:
|
case RSP_DX:
|
||||||
return new RspDxTunerConfiguration(uniqueID);
|
return new RspDxTunerConfiguration(uniqueID);
|
||||||
|
case SIGNAL_HOUND:
|
||||||
|
return new SignalHoundTunerConfiguration(uniqueID);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unrecognized tuner type [" + type.name() + "]");
|
throw new IllegalArgumentException("Unrecognized tuner type [" + type.name() + "]");
|
||||||
}
|
}
|
||||||
@@ -448,6 +470,13 @@ public class TunerFactory
|
|||||||
discoveredTuner.getClass());
|
discoveredTuner.getClass());
|
||||||
case RECORDING_TUNER:
|
case RECORDING_TUNER:
|
||||||
return new RecordingTunerEditor(userPreferences, tunerManager, discoveredTuner);
|
return new RecordingTunerEditor(userPreferences, tunerManager, discoveredTuner);
|
||||||
|
case SIGNAL_HOUND:
|
||||||
|
if(discoveredTuner instanceof DiscoveredSignalHoundTuner discoveredSignalHoundTuner)
|
||||||
|
{
|
||||||
|
return new SignalHoundTunerEditor(userPreferences, tunerManager, discoveredSignalHoundTuner);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unrecognized discovered Signal Hound tuner class: " +
|
||||||
|
discoveredTuner.getClass());
|
||||||
case RTL2832:
|
case RTL2832:
|
||||||
if(discoveredTuner.hasTuner())
|
if(discoveredTuner.hasTuner())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public enum TunerType
|
|||||||
RSP_DUO_1("RSPduo Tuner 1"),
|
RSP_DUO_1("RSPduo Tuner 1"),
|
||||||
RSP_DUO_2("RSPduo Tuner 2"),
|
RSP_DUO_2("RSPduo Tuner 2"),
|
||||||
RSP_DX("RSPdx"),
|
RSP_DX("RSPdx"),
|
||||||
|
SIGNAL_HOUND("Signal Hound"),
|
||||||
|
|
||||||
TEST("Test"),
|
TEST("Test"),
|
||||||
RECORDING("Recording"),
|
RECORDING("Recording"),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import io.github.dsheirer.source.tuner.rtl.fc2580.FC2580TunerConfiguration;
|
|||||||
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TTunerConfiguration;
|
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TTunerConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DTunerConfiguration;
|
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DTunerConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
|
import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.SignalHoundTunerConfiguration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class to hold a configuration for a specific type of tuner
|
* Abstract class to hold a configuration for a specific type of tuner
|
||||||
@@ -62,6 +63,7 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
|
|||||||
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
|
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
|
||||||
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),
|
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),
|
||||||
@JsonSubTypes.Type(value = RspTunerConfiguration.class, name = "rspTunerConfiguration"),
|
@JsonSubTypes.Type(value = RspTunerConfiguration.class, name = "rspTunerConfiguration"),
|
||||||
|
@JsonSubTypes.Type(value = SignalHoundTunerConfiguration.class, name = "signalHoundTunerConfiguration"),
|
||||||
})
|
})
|
||||||
@JacksonXmlRootElement(localName = "tuner_configuration")
|
@JacksonXmlRootElement(localName = "tuner_configuration")
|
||||||
public abstract class TunerConfiguration
|
public abstract class TunerConfiguration
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import io.github.dsheirer.preference.source.ChannelizerType;
|
|||||||
import io.github.dsheirer.sdrplay.DeviceInfo;
|
import io.github.dsheirer.sdrplay.DeviceInfo;
|
||||||
import io.github.dsheirer.sdrplay.SDRplay;
|
import io.github.dsheirer.sdrplay.SDRplay;
|
||||||
import io.github.dsheirer.sdrplay.SDRplayException;
|
import io.github.dsheirer.sdrplay.SDRplayException;
|
||||||
|
import io.github.dsheirer.signalhound.DeviceDescriptor;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHound;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundApi;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundException;
|
||||||
import io.github.dsheirer.source.Source;
|
import io.github.dsheirer.source.Source;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||||
@@ -50,6 +54,7 @@ import io.github.dsheirer.source.tuner.configuration.TunerConfigurationManager;
|
|||||||
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
|
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
|
import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.DiscoveredRspDuoTuner1;
|
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.DiscoveredRspDuoTuner1;
|
||||||
|
import io.github.dsheirer.source.tuner.signalhound.DiscoveredSignalHoundTuner;
|
||||||
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
|
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -136,6 +141,8 @@ public class TunerManager implements IDiscoveredTunerStatusListener
|
|||||||
discoverSdrPlayTuners();
|
discoverSdrPlayTuners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//The Signal Hound API libraries link their own libusb (and FTDI library for the BB60), so they don't need jlibusb
|
||||||
|
discoverSignalHoundTuners();
|
||||||
discoverRecordingTuners();
|
discoverRecordingTuners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,6 +397,57 @@ public class TunerManager implements IDiscoveredTunerStatusListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover Signal Hound tuners (BB60, SM200/SM435 and SP145) with each of the Signal Hound API libraries that is
|
||||||
|
* installed. The libraries are from the Signal Hound SDK and are installed separately. Networked (10GbE) SM
|
||||||
|
* devices aren't discovered.
|
||||||
|
*/
|
||||||
|
private void discoverSignalHoundTuners()
|
||||||
|
{
|
||||||
|
ChannelizerType channelizerType = mUserPreferences.getTunerPreference().getChannelizerType();
|
||||||
|
List<String> notInstalled = new ArrayList<>();
|
||||||
|
|
||||||
|
for(SignalHoundApi api : SignalHoundApi.values())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(!api.isAvailable())
|
||||||
|
{
|
||||||
|
notInstalled.add(api.libraryName());
|
||||||
|
mLog.debug("Signal Hound " + api.libraryName() + " library not loaded - " +
|
||||||
|
api.loadFailure().orElse("unknown reason"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mLog.info("Signal Hound " + api.libraryName() + " version [" + api.version() + "] loaded from [" +
|
||||||
|
api.location().orElse("unknown") + "]");
|
||||||
|
|
||||||
|
List<DeviceDescriptor> devices = SignalHound.devices(api);
|
||||||
|
mLog.info("Discovered [" + devices.size() + "] Signal Hound " + api.description() + " devices");
|
||||||
|
|
||||||
|
for(DeviceDescriptor device : devices)
|
||||||
|
{
|
||||||
|
DiscoveredSignalHoundTuner tuner = new DiscoveredSignalHoundTuner(device, channelizerType);
|
||||||
|
|
||||||
|
if(mDiscoveredTunerModel.getDiscoveredTuner(tuner.getId()) == null)
|
||||||
|
{
|
||||||
|
startAndConfigureTuner(tuner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(SignalHoundException | RuntimeException | LinkageError e)
|
||||||
|
{
|
||||||
|
mLog.error("Unable to discover Signal Hound " + api.description() + " devices - " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!notInstalled.isEmpty())
|
||||||
|
{
|
||||||
|
mLog.info("Signal Hound API libraries not installed " + notInstalled + " - Signal Hound tuners for these " +
|
||||||
|
"libraries are not available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the SDRplay API discovery session.
|
* Closes the SDRplay API discovery session.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import io.github.dsheirer.preference.source.ChannelizerType;
|
||||||
|
import io.github.dsheirer.signalhound.DeviceDescriptor;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerClass;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerFactory;
|
||||||
|
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound tuner discovered from the device list of a Signal Hound API.
|
||||||
|
*/
|
||||||
|
public class DiscoveredSignalHoundTuner extends DiscoveredTuner
|
||||||
|
{
|
||||||
|
private final DeviceDescriptor mDescriptor;
|
||||||
|
private final ChannelizerType mChannelizerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param descriptor of the device, from the Signal Hound API device list
|
||||||
|
* @param channelizerType to use for the tuner
|
||||||
|
*/
|
||||||
|
public DiscoveredSignalHoundTuner(DeviceDescriptor descriptor, ChannelizerType channelizerType)
|
||||||
|
{
|
||||||
|
mDescriptor = descriptor;
|
||||||
|
mChannelizerType = channelizerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor of the device
|
||||||
|
*/
|
||||||
|
public DeviceDescriptor getDescriptor()
|
||||||
|
{
|
||||||
|
return mDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device family
|
||||||
|
*/
|
||||||
|
public SignalHoundFamily getFamily()
|
||||||
|
{
|
||||||
|
return SignalHoundFamily.of(mDescriptor.api());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TunerClass getTunerClass()
|
||||||
|
{
|
||||||
|
return TunerClass.SIGNAL_HOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs and starts the tuner
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void start()
|
||||||
|
{
|
||||||
|
if(isAvailable() && !hasTuner())
|
||||||
|
{
|
||||||
|
mTuner = TunerFactory.getSignalHoundTuner(mDescriptor, mChannelizerType, this);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mTuner.start();
|
||||||
|
}
|
||||||
|
catch(Exception se)
|
||||||
|
{
|
||||||
|
setErrorMessage("Error starting tuner [" + getId() + "] - " + se.getMessage());
|
||||||
|
mTuner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tuner ID, which identifies the tuner configuration. The family is used instead of the model, since the APIs
|
||||||
|
* don't always report the model before the device is opened, and the ID must be the same each time.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return getId(mDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tuner ID of a device.
|
||||||
|
*/
|
||||||
|
public static String getId(DeviceDescriptor descriptor)
|
||||||
|
{
|
||||||
|
return "Signal Hound " + SignalHoundFamily.of(descriptor.api()) + " SER#" + descriptor.serialNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound device details for display.
|
||||||
|
* @param model such as BB60D, SM200B or SP145
|
||||||
|
* @param serial number
|
||||||
|
* @param firmware version
|
||||||
|
* @param library name of the Signal Hound API library, such as bb_api
|
||||||
|
* @param libraryVersion of the Signal Hound API library
|
||||||
|
* @param libraryLocation where the Signal Hound API library was loaded from
|
||||||
|
*/
|
||||||
|
public record SignalHoundDeviceInformation(String model, int serial, String firmware, String library,
|
||||||
|
String libraryVersion, String libraryLocation)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundApi;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound device families, one for each Signal Hound API, with the limits and front-end controls of each family
|
||||||
|
* from the SDK headers (bb_api.h, sm_api.h and sp_api.h).
|
||||||
|
*
|
||||||
|
* The family names are used in tuner IDs, which persist the tuner configurations, so they must not change.
|
||||||
|
*/
|
||||||
|
public enum SignalHoundFamily
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* BB60A, BB60C and BB60D. Gain (0-3) and attenuation (0-30 dB in 10 dB steps) are automatic unless both are set.
|
||||||
|
*/
|
||||||
|
BB60(SignalHoundApi.BB, "BB60", 40_000_000, 9_000L, 6_400_000_000L, 3, 10, true, false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM200A/B/C and SM435B/C. Attenuation 0-30 dB in 5 dB steps, and an RF preselector up to 645 MHz.
|
||||||
|
*/
|
||||||
|
SM(SignalHoundApi.SM, "SM", 50_000_000, 100_000L, 20_600_000_000L, 6, 5, false, true),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SP145A and SP145B. Attenuation 0-30 dB in 5 dB steps, and an RF preselector on the SP145B up to 520 MHz.
|
||||||
|
*/
|
||||||
|
SP145(SignalHoundApi.SP, "SP145", 61_440_000, 9_000L, 15_000_000_000L, 6, 5, false, true);
|
||||||
|
|
||||||
|
//SM435 models tune higher than the SM200
|
||||||
|
private static final long SM435_MAXIMUM_FREQUENCY = 44_200_000_000L;
|
||||||
|
|
||||||
|
private final SignalHoundApi mApi;
|
||||||
|
private final String mLabel;
|
||||||
|
private final int mBaseSampleRate;
|
||||||
|
private final long mMinimumFrequency;
|
||||||
|
private final long mMaximumFrequency;
|
||||||
|
private final int mMaximumAttenuation;
|
||||||
|
private final int mAttenuationStepDb;
|
||||||
|
private final boolean mHasGain;
|
||||||
|
private final boolean mHasPreselector;
|
||||||
|
|
||||||
|
SignalHoundFamily(SignalHoundApi api, String label, int baseSampleRate, long minimumFrequency,
|
||||||
|
long maximumFrequency, int maximumAttenuation, int attenuationStepDb, boolean hasGain,
|
||||||
|
boolean hasPreselector)
|
||||||
|
{
|
||||||
|
mApi = api;
|
||||||
|
mLabel = label;
|
||||||
|
mBaseSampleRate = baseSampleRate;
|
||||||
|
mMinimumFrequency = minimumFrequency;
|
||||||
|
mMaximumFrequency = maximumFrequency;
|
||||||
|
mMaximumAttenuation = maximumAttenuation;
|
||||||
|
mAttenuationStepDb = attenuationStepDb;
|
||||||
|
mHasGain = hasGain;
|
||||||
|
mHasPreselector = hasPreselector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Family of the devices that an API drives.
|
||||||
|
*/
|
||||||
|
public static SignalHoundFamily of(SignalHoundApi api)
|
||||||
|
{
|
||||||
|
return switch(api)
|
||||||
|
{
|
||||||
|
case BB -> BB60;
|
||||||
|
case SM -> SM;
|
||||||
|
case SP -> SP145;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API that drives the family.
|
||||||
|
*/
|
||||||
|
public SignalHoundApi getApi()
|
||||||
|
{
|
||||||
|
return mApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I/Q sample rate before decimation, in Hertz.
|
||||||
|
*/
|
||||||
|
public int getBaseSampleRate()
|
||||||
|
{
|
||||||
|
return mBaseSampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lowest tunable frequency, in Hertz.
|
||||||
|
*/
|
||||||
|
public long getMinimumFrequency()
|
||||||
|
{
|
||||||
|
return mMinimumFrequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highest tunable frequency of the family's default model, in Hertz.
|
||||||
|
*/
|
||||||
|
public long getMaximumFrequency()
|
||||||
|
{
|
||||||
|
return mMaximumFrequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highest tunable frequency of a model, in Hertz.
|
||||||
|
* @param model as reported by the device, such as SM435B
|
||||||
|
*/
|
||||||
|
public long getMaximumFrequency(String model)
|
||||||
|
{
|
||||||
|
if(this == SM && model != null && model.toUpperCase(Locale.ROOT).startsWith("SM435"))
|
||||||
|
{
|
||||||
|
return SM435_MAXIMUM_FREQUENCY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mMaximumFrequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highest attenuator setting, where each setting is {@link #getAttenuationStepDb()} dB.
|
||||||
|
*/
|
||||||
|
public int getMaximumAttenuation()
|
||||||
|
{
|
||||||
|
return mMaximumAttenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attenuation in dB of each attenuator setting.
|
||||||
|
*/
|
||||||
|
public int getAttenuationStepDb()
|
||||||
|
{
|
||||||
|
return mAttenuationStepDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the family has a manual gain setting, in addition to the attenuator.
|
||||||
|
*/
|
||||||
|
public boolean hasGain()
|
||||||
|
{
|
||||||
|
return mHasGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highest gain setting, for families that have one.
|
||||||
|
*/
|
||||||
|
public int getMaximumGain()
|
||||||
|
{
|
||||||
|
return mHasGain ? 3 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if devices of the family can have an RF preselector. For the SP145, only the SP145B has one.
|
||||||
|
*/
|
||||||
|
public boolean hasPreselector()
|
||||||
|
{
|
||||||
|
return mHasPreselector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return mLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound I/Q sample rate: the family's base sample rate divided by a power of two decimation.
|
||||||
|
* @param decimation power of two
|
||||||
|
* @param rate in Hertz
|
||||||
|
*/
|
||||||
|
public record SignalHoundSampleRate(int decimation, int rate)
|
||||||
|
{
|
||||||
|
//Decimations offered to the user. Higher decimations are supported by the APIs, but the sample rates are below
|
||||||
|
//what is useful for channelizing (600-780 kHz at the highest decimation offered here).
|
||||||
|
public static final int MAXIMUM_DECIMATION = 64;
|
||||||
|
public static final int DEFAULT_RATE = 10_000_000;
|
||||||
|
private static final DecimalFormat FORMAT = new DecimalFormat("0.00###");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample rates of a family, highest rate first. Every rate is an integer number of Hertz.
|
||||||
|
*/
|
||||||
|
public static List<SignalHoundSampleRate> getRates(SignalHoundFamily family)
|
||||||
|
{
|
||||||
|
List<SignalHoundSampleRate> rates = new ArrayList<>();
|
||||||
|
|
||||||
|
for(int decimation = 1; decimation <= MAXIMUM_DECIMATION; decimation *= 2)
|
||||||
|
{
|
||||||
|
rates.add(new SignalHoundSampleRate(decimation, family.getBaseSampleRate() / decimation));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the highest rate that doesn't exceed the requested rate.
|
||||||
|
* @param rates to search
|
||||||
|
* @param requested rate in Hertz
|
||||||
|
* @return closest rate, or the lowest rate when the requested rate is below all of them
|
||||||
|
*/
|
||||||
|
public static SignalHoundSampleRate getClosest(List<SignalHoundSampleRate> rates, int requested)
|
||||||
|
{
|
||||||
|
SignalHoundSampleRate closest = null;
|
||||||
|
SignalHoundSampleRate lowest = null;
|
||||||
|
|
||||||
|
for(SignalHoundSampleRate rate : rates)
|
||||||
|
{
|
||||||
|
if(rate.rate() <= requested && (closest == null || rate.rate() > closest.rate()))
|
||||||
|
{
|
||||||
|
closest = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lowest == null || rate.rate() < lowest.rate())
|
||||||
|
{
|
||||||
|
lowest = rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest != null ? closest : lowest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return FORMAT.format(rate / 1E6) + " MHz";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import io.github.dsheirer.preference.source.ChannelizerType;
|
||||||
|
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
||||||
|
import io.github.dsheirer.source.tuner.Tuner;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound tuner: BB60, SM200/SM435 or SP145
|
||||||
|
*/
|
||||||
|
public class SignalHoundTuner extends Tuner
|
||||||
|
{
|
||||||
|
//The BB60 and SM200 have 14-bit ADCs. The samples are floats after the API's corrections and filtering.
|
||||||
|
private static final double SAMPLE_SIZE_14_BIT = 14.0;
|
||||||
|
//The API transfers 16-bit I and Q samples from the device (32 bits per complex sample)
|
||||||
|
private static final int LINK_BITS_PER_SAMPLE = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param controller for the Signal Hound device
|
||||||
|
* @param tunerErrorListener to listen for errors from this tuner
|
||||||
|
* @param channelizerType for the channelizer
|
||||||
|
*/
|
||||||
|
public SignalHoundTuner(SignalHoundTunerController controller, ITunerErrorListener tunerErrorListener,
|
||||||
|
ChannelizerType channelizerType)
|
||||||
|
{
|
||||||
|
super(controller, tunerErrorListener, channelizerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound tuner controller
|
||||||
|
*/
|
||||||
|
public SignalHoundTunerController getController()
|
||||||
|
{
|
||||||
|
return (SignalHoundTunerController)getTunerController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPreferredName()
|
||||||
|
{
|
||||||
|
SignalHoundDeviceInformation info = getController().getDeviceInfo();
|
||||||
|
String model = info != null ? info.model() : getController().getFamily().toString();
|
||||||
|
return "Signal Hound " + model + " " + getUniqueID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUniqueID()
|
||||||
|
{
|
||||||
|
return Integer.toString(getController().getDescriptor().serialNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TunerClass getTunerClass()
|
||||||
|
{
|
||||||
|
return TunerClass.SIGNAL_HOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getSampleSize()
|
||||||
|
{
|
||||||
|
return SAMPLE_SIZE_14_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaximumUSBBitsPerSecond()
|
||||||
|
{
|
||||||
|
return getController().getFamily().getBaseSampleRate() * LINK_BITS_PER_SAMPLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerType;
|
||||||
|
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound tuner configuration, for devices of all Signal Hound families.
|
||||||
|
*/
|
||||||
|
public class SignalHoundTunerConfiguration extends TunerConfiguration
|
||||||
|
{
|
||||||
|
private int mSampleRate = SignalHoundSampleRate.DEFAULT_RATE;
|
||||||
|
private int mReferenceLevel = SignalHoundTunerController.REFERENCE_LEVEL_DEFAULT;
|
||||||
|
private int mAttenuation = SignalHoundTunerController.AUTOMATIC;
|
||||||
|
private int mGain = SignalHoundTunerController.AUTOMATIC;
|
||||||
|
private boolean mPreselector = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor for Jackson
|
||||||
|
*/
|
||||||
|
public SignalHoundTunerConfiguration()
|
||||||
|
{
|
||||||
|
super(SignalHoundTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ,
|
||||||
|
SignalHoundTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param uniqueID for the tuner
|
||||||
|
*/
|
||||||
|
public SignalHoundTunerConfiguration(String uniqueID)
|
||||||
|
{
|
||||||
|
super(uniqueID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
public TunerType getTunerType()
|
||||||
|
{
|
||||||
|
return TunerType.SIGNAL_HOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample rate in Hertz
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
|
||||||
|
public int getSampleRate()
|
||||||
|
{
|
||||||
|
return mSampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSampleRate(int sampleRate)
|
||||||
|
{
|
||||||
|
mSampleRate = sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference level in dBm: the strongest expected input level, which sets the gain and attenuation automatically.
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "reference_level")
|
||||||
|
public int getReferenceLevel()
|
||||||
|
{
|
||||||
|
return mReferenceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReferenceLevel(int referenceLevel)
|
||||||
|
{
|
||||||
|
mReferenceLevel = referenceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attenuator setting, or -1 for automatic. The step size depends on the device family.
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "attenuation")
|
||||||
|
public int getAttenuation()
|
||||||
|
{
|
||||||
|
return mAttenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttenuation(int attenuation)
|
||||||
|
{
|
||||||
|
mAttenuation = attenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gain setting (BB60 only), or -1 for automatic.
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "gain")
|
||||||
|
public int getGain()
|
||||||
|
{
|
||||||
|
return mGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGain(int gain)
|
||||||
|
{
|
||||||
|
mGain = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RF preselector (SM200/SM435 and SP145B) enabled.
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "preselector")
|
||||||
|
public boolean isPreselector()
|
||||||
|
{
|
||||||
|
return mPreselector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreselector(boolean preselector)
|
||||||
|
{
|
||||||
|
mPreselector = preselector;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,534 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.signalhound.Diagnostics;
|
||||||
|
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||||
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import io.github.dsheirer.source.tuner.manager.TunerStatus;
|
||||||
|
import io.github.dsheirer.source.tuner.ui.TunerEditor;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.swing.DefaultComboBoxModel;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JSeparator;
|
||||||
|
import javax.swing.JSlider;
|
||||||
|
import javax.swing.SpinnerNumberModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal Hound tuner editor
|
||||||
|
*/
|
||||||
|
public class SignalHoundTunerEditor extends TunerEditor<SignalHoundTuner, SignalHoundTunerConfiguration>
|
||||||
|
{
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final Logger mLog = LoggerFactory.getLogger(SignalHoundTunerEditor.class);
|
||||||
|
private final SignalHoundFamily mFamily;
|
||||||
|
private JButton mTunerInfoButton;
|
||||||
|
private JComboBox<SignalHoundSampleRate> mSampleRateCombo;
|
||||||
|
private JSlider mReferenceLevelSlider;
|
||||||
|
private JLabel mReferenceLevelValueLabel;
|
||||||
|
private JComboBox<Setting> mAttenuationCombo;
|
||||||
|
private JComboBox<Setting> mGainCombo;
|
||||||
|
private JCheckBox mPreselectorCheckBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param userPreferences for wide-band recordings
|
||||||
|
* @param tunerManager for saving tuner configuration
|
||||||
|
* @param discoveredTuner for the optionally usable Signal Hound tuner and controller
|
||||||
|
*/
|
||||||
|
public SignalHoundTunerEditor(UserPreferences userPreferences, TunerManager tunerManager,
|
||||||
|
DiscoveredSignalHoundTuner discoveredTuner)
|
||||||
|
{
|
||||||
|
super(userPreferences, tunerManager, discoveredTuner);
|
||||||
|
mFamily = discoveredTuner.getFamily();
|
||||||
|
init();
|
||||||
|
tunerStatusUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMinimumTunableFrequency()
|
||||||
|
{
|
||||||
|
return hasTuner() ? getTuner().getController().getMinimumTunableFrequency() :
|
||||||
|
SignalHoundTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMaximumTunableFrequency()
|
||||||
|
{
|
||||||
|
return hasTuner() ? getTuner().getController().getMaximumTunableFrequency() :
|
||||||
|
SignalHoundTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init()
|
||||||
|
{
|
||||||
|
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][][][grow]"));
|
||||||
|
|
||||||
|
add(new JLabel("Tuner:"));
|
||||||
|
add(getTunerIdLabel());
|
||||||
|
add(getTunerInfoButton());
|
||||||
|
|
||||||
|
add(new JLabel("Status:"));
|
||||||
|
add(getTunerStatusLabel(), "wrap");
|
||||||
|
|
||||||
|
add(getButtonPanel(), "span,align left");
|
||||||
|
|
||||||
|
add(new JSeparator(), "span,growx");
|
||||||
|
|
||||||
|
add(new JLabel("Frequency (MHz):"));
|
||||||
|
add(getFrequencyPanel(), "wrap");
|
||||||
|
|
||||||
|
add(new JLabel("Sample Rate:"));
|
||||||
|
add(getSampleRateCombo(), "wrap");
|
||||||
|
|
||||||
|
add(new JSeparator(), "span,growx");
|
||||||
|
|
||||||
|
add(new JLabel("Reference Level:"));
|
||||||
|
add(getReferenceLevelSlider());
|
||||||
|
add(getReferenceLevelValueLabel());
|
||||||
|
|
||||||
|
add(new JLabel("Attenuation:"));
|
||||||
|
add(getAttenuationCombo(), "wrap");
|
||||||
|
|
||||||
|
if(mFamily.hasGain())
|
||||||
|
{
|
||||||
|
add(new JLabel("Gain:"));
|
||||||
|
add(getGainCombo(), "wrap");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mFamily.hasPreselector())
|
||||||
|
{
|
||||||
|
add(new JLabel(""));
|
||||||
|
add(getPreselectorCheckBox(), "wrap");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Empty row that takes all extra vertical space so that the controls stay stacked at the top of the editor.
|
||||||
|
add(new JLabel(), "span,pushy");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tunerStatusUpdated()
|
||||||
|
{
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
boolean started = hasStartedTuner();
|
||||||
|
getTunerIdLabel().setText(started ? getTuner().getPreferredName() : getDiscoveredTuner().getId());
|
||||||
|
|
||||||
|
String status = getDiscoveredTuner().getTunerStatus().toString();
|
||||||
|
if(getDiscoveredTuner().hasErrorMessage())
|
||||||
|
{
|
||||||
|
status += " - " + getDiscoveredTuner().getErrorMessage();
|
||||||
|
}
|
||||||
|
getTunerStatusLabel().setText(status);
|
||||||
|
getButtonPanel().updateControls();
|
||||||
|
getFrequencyPanel().updateControls();
|
||||||
|
getTunerInfoButton().setEnabled(started);
|
||||||
|
|
||||||
|
if(started)
|
||||||
|
{
|
||||||
|
SignalHoundTunerController controller = getTuner().getController();
|
||||||
|
List<SignalHoundSampleRate> rates = controller.getSampleRates();
|
||||||
|
getSampleRateCombo().setModel(new DefaultComboBoxModel<>(rates.toArray(new SignalHoundSampleRate[0])));
|
||||||
|
getSampleRateCombo().setSelectedItem(controller.getSignalHoundSampleRate());
|
||||||
|
getSampleRateCombo().setEnabled(!controller.isLockedSampleRate());
|
||||||
|
|
||||||
|
getReferenceLevelSlider().setValue(controller.getReferenceLevel());
|
||||||
|
getReferenceLevelSlider().setEnabled(true);
|
||||||
|
getReferenceLevelValueLabel().setEnabled(true);
|
||||||
|
|
||||||
|
getAttenuationCombo().setSelectedItem(new Setting(controller.getAttenuation(), null));
|
||||||
|
getAttenuationCombo().setEnabled(true);
|
||||||
|
|
||||||
|
if(mFamily.hasGain())
|
||||||
|
{
|
||||||
|
getGainCombo().setSelectedItem(new Setting(controller.getGain(), null));
|
||||||
|
getGainCombo().setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mFamily.hasPreselector())
|
||||||
|
{
|
||||||
|
getPreselectorCheckBox().setSelected(controller.isPreselector());
|
||||||
|
getPreselectorCheckBox().setEnabled(controller.isPreselectorSupported());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getSampleRateCombo().setModel(new DefaultComboBoxModel<>());
|
||||||
|
getSampleRateCombo().setEnabled(false);
|
||||||
|
getReferenceLevelSlider().setEnabled(false);
|
||||||
|
getReferenceLevelValueLabel().setEnabled(false);
|
||||||
|
getAttenuationCombo().setEnabled(false);
|
||||||
|
|
||||||
|
if(mFamily.hasGain())
|
||||||
|
{
|
||||||
|
getGainCombo().setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mFamily.hasPreselector())
|
||||||
|
{
|
||||||
|
getPreselectorCheckBox().setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReferenceLevelLabel();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the tuner has finished starting, since a status update can arrive while it's starting or restarting.
|
||||||
|
*/
|
||||||
|
private boolean hasStartedTuner()
|
||||||
|
{
|
||||||
|
return hasTuner() && getTuner().getController().getSignalHoundSampleRate() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateReferenceLevelLabel()
|
||||||
|
{
|
||||||
|
getReferenceLevelValueLabel().setText(getReferenceLevelSlider().getValue() + " dBm");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that provides tuner information
|
||||||
|
*/
|
||||||
|
private JButton getTunerInfoButton()
|
||||||
|
{
|
||||||
|
if(mTunerInfoButton == null)
|
||||||
|
{
|
||||||
|
mTunerInfoButton = new JButton("Info");
|
||||||
|
mTunerInfoButton.setEnabled(false);
|
||||||
|
mTunerInfoButton.addActionListener(_ -> JOptionPane.showMessageDialog(SignalHoundTunerEditor.this,
|
||||||
|
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTunerInfoButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComboBox<SignalHoundSampleRate> getSampleRateCombo()
|
||||||
|
{
|
||||||
|
if(mSampleRateCombo == null)
|
||||||
|
{
|
||||||
|
mSampleRateCombo = new JComboBox<>();
|
||||||
|
mSampleRateCombo.setEnabled(false);
|
||||||
|
mSampleRateCombo.addActionListener(_ ->
|
||||||
|
{
|
||||||
|
if(hasTuner() && !isLoading())
|
||||||
|
{
|
||||||
|
SignalHoundSampleRate rate = (SignalHoundSampleRate)mSampleRateCombo.getSelectedItem();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getTuner().getController().setSampleRate(rate);
|
||||||
|
|
||||||
|
//Adjust the min/max values for the sample rate.
|
||||||
|
adjustForSampleRate(rate.rate());
|
||||||
|
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
catch(Exception e1)
|
||||||
|
{
|
||||||
|
mLog.error("Error setting Signal Hound sample rate", e1);
|
||||||
|
JOptionPane.showMessageDialog(SignalHoundTunerEditor.this, "Couldn't set sample rate to " +
|
||||||
|
rate + " - " + e1.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSampleRateCombo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSlider getReferenceLevelSlider()
|
||||||
|
{
|
||||||
|
if(mReferenceLevelSlider == null)
|
||||||
|
{
|
||||||
|
mReferenceLevelSlider = new JSlider(JSlider.HORIZONTAL, SignalHoundTunerController.REFERENCE_LEVEL_MINIMUM,
|
||||||
|
SignalHoundTunerController.REFERENCE_LEVEL_MAXIMUM,
|
||||||
|
SignalHoundTunerController.REFERENCE_LEVEL_DEFAULT);
|
||||||
|
mReferenceLevelSlider.setEnabled(false);
|
||||||
|
mReferenceLevelSlider.setMajorTickSpacing(10);
|
||||||
|
mReferenceLevelSlider.setPaintTicks(true);
|
||||||
|
mReferenceLevelSlider.setToolTipText("Strongest expected input level. Lower values increase the gain. " +
|
||||||
|
"Raise it when the log reports ADC overflow warnings.");
|
||||||
|
mReferenceLevelSlider.addChangeListener(_ ->
|
||||||
|
{
|
||||||
|
updateReferenceLevelLabel();
|
||||||
|
|
||||||
|
//Reconfigure the stream once the user releases the slider, since each change restarts the stream
|
||||||
|
if(hasTuner() && !isLoading() && !mReferenceLevelSlider.getValueIsAdjusting())
|
||||||
|
{
|
||||||
|
int referenceLevel = mReferenceLevelSlider.getValue();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getTuner().getController().setReferenceLevel(referenceLevel);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't set Signal Hound reference level to:" + referenceLevel, e);
|
||||||
|
JOptionPane.showMessageDialog(mReferenceLevelSlider, "Couldn't set reference level to " +
|
||||||
|
referenceLevel + " dBm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mReferenceLevelSlider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JLabel getReferenceLevelValueLabel()
|
||||||
|
{
|
||||||
|
if(mReferenceLevelValueLabel == null)
|
||||||
|
{
|
||||||
|
mReferenceLevelValueLabel = new JLabel("0 dBm");
|
||||||
|
mReferenceLevelValueLabel.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mReferenceLevelValueLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComboBox<Setting> getAttenuationCombo()
|
||||||
|
{
|
||||||
|
if(mAttenuationCombo == null)
|
||||||
|
{
|
||||||
|
List<Setting> settings = new ArrayList<>();
|
||||||
|
settings.add(new Setting(SignalHoundTunerController.AUTOMATIC, "Automatic (Reference Level)"));
|
||||||
|
|
||||||
|
for(int x = 0; x <= mFamily.getMaximumAttenuation(); x++)
|
||||||
|
{
|
||||||
|
settings.add(new Setting(x, (x * mFamily.getAttenuationStepDb()) + " dB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
mAttenuationCombo = new JComboBox<>(settings.toArray(new Setting[0]));
|
||||||
|
mAttenuationCombo.setEnabled(false);
|
||||||
|
mAttenuationCombo.setToolTipText(mFamily.hasGain() ? "The BB60 uses manual attenuation only when the " +
|
||||||
|
"gain is also manual" : "Input attenuation, or automatic to set it from the reference level");
|
||||||
|
mAttenuationCombo.addActionListener(_ ->
|
||||||
|
{
|
||||||
|
if(hasTuner() && !isLoading() && mAttenuationCombo.getSelectedItem() instanceof Setting setting)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getTuner().getController().setAttenuation(setting.value());
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't set Signal Hound attenuation to:" + setting, e);
|
||||||
|
JOptionPane.showMessageDialog(mAttenuationCombo, "Couldn't set attenuation to " + setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mAttenuationCombo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComboBox<Setting> getGainCombo()
|
||||||
|
{
|
||||||
|
if(mGainCombo == null)
|
||||||
|
{
|
||||||
|
List<Setting> settings = new ArrayList<>();
|
||||||
|
settings.add(new Setting(SignalHoundTunerController.AUTOMATIC, "Automatic (Reference Level)"));
|
||||||
|
|
||||||
|
for(int x = 0; x <= mFamily.getMaximumGain(); x++)
|
||||||
|
{
|
||||||
|
settings.add(new Setting(x, Integer.toString(x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mGainCombo = new JComboBox<>(settings.toArray(new Setting[0]));
|
||||||
|
mGainCombo.setEnabled(false);
|
||||||
|
mGainCombo.setToolTipText("The BB60 uses manual gain only when the attenuation is also manual");
|
||||||
|
mGainCombo.addActionListener(_ ->
|
||||||
|
{
|
||||||
|
if(hasTuner() && !isLoading() && mGainCombo.getSelectedItem() instanceof Setting setting)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getTuner().getController().setGain(setting.value());
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't set Signal Hound gain to:" + setting, e);
|
||||||
|
JOptionPane.showMessageDialog(mGainCombo, "Couldn't set gain to " + setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mGainCombo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JCheckBox getPreselectorCheckBox()
|
||||||
|
{
|
||||||
|
if(mPreselectorCheckBox == null)
|
||||||
|
{
|
||||||
|
mPreselectorCheckBox = new JCheckBox("RF Preselector");
|
||||||
|
mPreselectorCheckBox.setEnabled(false);
|
||||||
|
mPreselectorCheckBox.setToolTipText(mFamily == SignalHoundFamily.SM ?
|
||||||
|
"Filters the input to reject strong out-of-band signals, up to 645 MHz" :
|
||||||
|
"SP145B only: filters the input to reject strong out-of-band signals, up to 520 MHz");
|
||||||
|
mPreselectorCheckBox.addActionListener(_ ->
|
||||||
|
{
|
||||||
|
if(hasTuner() && !isLoading())
|
||||||
|
{
|
||||||
|
boolean enabled = mPreselectorCheckBox.isSelected();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getTuner().getController().setPreselector(enabled);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't " + (enabled ? "enable" : "disable") + " Signal Hound preselector", e);
|
||||||
|
JOptionPane.showMessageDialog(mPreselectorCheckBox, "Couldn't change preselector - " +
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mPreselectorCheckBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
if(hasConfiguration() && !isLoading())
|
||||||
|
{
|
||||||
|
getConfiguration().setFrequency(getFrequencyControl().getFrequency());
|
||||||
|
getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency());
|
||||||
|
getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency());
|
||||||
|
double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
|
||||||
|
getConfiguration().setFrequencyCorrection(value);
|
||||||
|
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
|
||||||
|
|
||||||
|
if(getSampleRateCombo().getSelectedItem() instanceof SignalHoundSampleRate rate)
|
||||||
|
{
|
||||||
|
getConfiguration().setSampleRate(rate.rate());
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguration().setReferenceLevel(getReferenceLevelSlider().getValue());
|
||||||
|
|
||||||
|
if(getAttenuationCombo().getSelectedItem() instanceof Setting setting)
|
||||||
|
{
|
||||||
|
getConfiguration().setAttenuation(setting.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mFamily.hasGain() && getGainCombo().getSelectedItem() instanceof Setting setting)
|
||||||
|
{
|
||||||
|
getConfiguration().setGain(setting.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mFamily.hasPreselector())
|
||||||
|
{
|
||||||
|
getConfiguration().setPreselector(getPreselectorCheckBox().isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTunerLockState(boolean locked)
|
||||||
|
{
|
||||||
|
getFrequencyPanel().updateControls();
|
||||||
|
getSampleRateCombo().setEnabled(!locked);
|
||||||
|
getSampleRateCombo().setToolTipText(locked ? "Sample Rate is locked. Disable decoding channels to unlock." :
|
||||||
|
"Select a sample rate for the tuner");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTunerInfo()
|
||||||
|
{
|
||||||
|
if(getDiscoveredTuner().getTunerStatus() == TunerStatus.ERROR)
|
||||||
|
{
|
||||||
|
return getDiscoveredTuner().getErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasTuner())
|
||||||
|
{
|
||||||
|
SignalHoundDeviceInformation info = getTuner().getController().getDeviceInfo();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<html><h3>Signal Hound Tuner</h3>");
|
||||||
|
sb.append("<b>Model: </b>").append(info.model()).append("<br>");
|
||||||
|
sb.append("<b>Serial: </b>").append(info.serial()).append("<br>");
|
||||||
|
sb.append("<b>Firmware: </b>").append(info.firmware()).append("<br>");
|
||||||
|
sb.append("<b>Library: </b>").append(info.library()).append(" ").append(info.libraryVersion()).append("<br>");
|
||||||
|
sb.append("<b>Location: </b>").append(info.libraryLocation()).append("<br>");
|
||||||
|
|
||||||
|
Diagnostics diagnostics = getTuner().getController().getDiagnostics();
|
||||||
|
|
||||||
|
if(diagnostics != null)
|
||||||
|
{
|
||||||
|
sb.append("<b>Temperature: </b>").append(String.format(Locale.ROOT, "%.1f C",
|
||||||
|
diagnostics.temperature())).append("<br>");
|
||||||
|
sb.append("<b>Voltage: </b>").append(String.format(Locale.ROOT, "%.2f V",
|
||||||
|
diagnostics.voltage())).append("<br>");
|
||||||
|
sb.append("<b>Current: </b>").append(String.format(Locale.ROOT, "%.3f A",
|
||||||
|
diagnostics.current())).append("<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attenuation or gain combo box entry. Entries are equal when their values are equal, so that the entry for a
|
||||||
|
* value can be selected with a new instance.
|
||||||
|
* @param value of the setting, or -1 for automatic
|
||||||
|
* @param label to display
|
||||||
|
*/
|
||||||
|
record Setting(int value, String label)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
return o instanceof Setting other && other.value == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Integer.hashCode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +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.util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects more than one version of the same library jar on the class path.
|
|
||||||
*
|
|
||||||
* The release launch scripts use a 'lib/*' wildcard class path, so when a user unzips a new release over an older one,
|
|
||||||
* the stale jars from the older release stay on the class path next to the new ones. The JVM loads classes (and the
|
|
||||||
* tuner libraries extract their native libraries) from whichever jar sorts first, which can silently run an older
|
|
||||||
* library version, including native code with bugs that the newer version fixed.
|
|
||||||
*/
|
|
||||||
public class DuplicateLibraryDetector
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Splits a jar file name into the library name and version. The version starts at the first dash that is
|
|
||||||
* followed by a digit, a 'v' and a digit, or 'nightly' (e.g. guava-33.8.0-jre, api-v2.1.1, sdr-trunk-nightly).
|
|
||||||
*/
|
|
||||||
private static final Pattern JAR_NAME = Pattern.compile("^(.+?)-(v?\\d.*|nightly)\\.jar$");
|
|
||||||
|
|
||||||
private DuplicateLibraryDetector()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the libraries that have more than one version on this JVM's class path.
|
|
||||||
* @return map of library name to the sorted jar paths for that library, empty when there are no duplicates.
|
|
||||||
*/
|
|
||||||
public static Map<String,List<String>> findDuplicates()
|
|
||||||
{
|
|
||||||
String classPath = System.getProperty("java.class.path", "");
|
|
||||||
return findDuplicates(List.of(classPath.split(Pattern.quote(File.pathSeparator))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the libraries that have more than one version in the class path entries. Entries that are not jar files
|
|
||||||
* or that don't have a version in the file name are ignored.
|
|
||||||
* @param classPathEntries to check
|
|
||||||
* @return map of library name to the sorted jar paths for that library, empty when there are no duplicates.
|
|
||||||
*/
|
|
||||||
public static Map<String,List<String>> findDuplicates(List<String> classPathEntries)
|
|
||||||
{
|
|
||||||
Map<String,TreeSet<String>> versions = new TreeMap<>();
|
|
||||||
Map<String,TreeSet<String>> paths = new TreeMap<>();
|
|
||||||
|
|
||||||
for(String entry: classPathEntries)
|
|
||||||
{
|
|
||||||
if(entry == null || entry.isBlank())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path fileName = Path.of(entry).getFileName();
|
|
||||||
|
|
||||||
if(fileName == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Matcher matcher = JAR_NAME.matcher(fileName.toString());
|
|
||||||
|
|
||||||
if(matcher.matches())
|
|
||||||
{
|
|
||||||
versions.computeIfAbsent(matcher.group(1), k -> new TreeSet<>()).add(matcher.group(2));
|
|
||||||
paths.computeIfAbsent(matcher.group(1), k -> new TreeSet<>()).add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String,List<String>> duplicates = new TreeMap<>();
|
|
||||||
|
|
||||||
for(Map.Entry<String,TreeSet<String>> entry: versions.entrySet())
|
|
||||||
{
|
|
||||||
if(entry.getValue().size() > 1)
|
|
||||||
{
|
|
||||||
duplicates.put(entry.getKey(), new ArrayList<>(paths.get(entry.getKey())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return duplicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a description of the duplicate libraries for a log entry or a dialog.
|
|
||||||
* @param duplicates from findDuplicates()
|
|
||||||
* @return description, one line per library, listing the jar file names for each library.
|
|
||||||
*/
|
|
||||||
public static String describe(Map<String,List<String>> duplicates)
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
for(Map.Entry<String,List<String>> entry: duplicates.entrySet())
|
|
||||||
{
|
|
||||||
List<String> names = entry.getValue().stream().map(p -> Path.of(p).getFileName().toString()).toList();
|
|
||||||
sb.append(" ").append(entry.getKey()).append(": ").append(String.join(", ", names)).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Folder that holds the duplicate jars, for telling the user which folder to replace.
|
|
||||||
* @param duplicates from findDuplicates()
|
|
||||||
* @return folder of the first duplicate jar, or null if there are no duplicates or the path has no parent.
|
|
||||||
*/
|
|
||||||
public static Path getFolder(Map<String,List<String>> duplicates)
|
|
||||||
{
|
|
||||||
if(duplicates.isEmpty())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.of(duplicates.values().iterator().next().getFirst()).toAbsolutePath().normalize().getParent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +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.edac;
|
|
||||||
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|
||||||
import java.util.Random;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests Golay(24,12) correction of the data bits of words that are not at the start of a 24-bit message.
|
|
||||||
*/
|
|
||||||
public class Golay24OffsetTest
|
|
||||||
{
|
|
||||||
private static int encode(int data)
|
|
||||||
{
|
|
||||||
int checksum = 0;
|
|
||||||
|
|
||||||
for(int bit = 0; bit < 12; bit++)
|
|
||||||
{
|
|
||||||
if((data & (1 << (11 - bit))) != 0)
|
|
||||||
{
|
|
||||||
checksum ^= Golay24.CHECKSUMS[bit];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int word23 = (data << 11) | checksum;
|
|
||||||
return (word23 << 1) | (Integer.bitCount(word23) & 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void correctsUpToThreeErrorsInEveryWordOfALongerMessage()
|
|
||||||
{
|
|
||||||
Random random = new Random(1);
|
|
||||||
|
|
||||||
for(int trial = 0; trial < 2000; trial++)
|
|
||||||
{
|
|
||||||
int[] words = new int[3];
|
|
||||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(72);
|
|
||||||
|
|
||||||
for(int w = 0; w < 3; w++)
|
|
||||||
{
|
|
||||||
words[w] = encode(random.nextInt(4096));
|
|
||||||
int received = words[w];
|
|
||||||
|
|
||||||
for(int e = random.nextInt(4); e > 0; e--)
|
|
||||||
{
|
|
||||||
received ^= 1 << random.nextInt(24);
|
|
||||||
}
|
|
||||||
|
|
||||||
message.load(w * 24, 24, received);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int w = 0; w < 3; w++)
|
|
||||||
{
|
|
||||||
Golay24.checkAndCorrect(message, w * 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int w = 0; w < 3; w++)
|
|
||||||
{
|
|
||||||
//The 12 data bits. (The core correction can leave the overall parity bit wrong when it corrects other
|
|
||||||
//bits, which no caller reads.)
|
|
||||||
assertEquals(words[w] >> 12, message.getInt(w * 24, w * 24 + 11), "trial " + trial + " word " + w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +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;
|
|
||||||
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.Identity;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.RequestParameter;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.RequestedTokens;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.Token;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.TokenType;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.TriggerCondition;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.TriggerInterval;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.TriggerPeriodic;
|
|
||||||
import java.util.List;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JUnit tests for LRRP request packet token parsing.
|
|
||||||
*
|
|
||||||
* The sample packets are Motorola triggered location start requests captured from a P25 system (UDP port 4001).
|
|
||||||
* Both carry the same requested fields (time, horizontal speed, horizontal direction) and the same two parameters of
|
|
||||||
* unknown meaning (0x44 = 100, 0x42 = 3600); one is a periodic trigger every 600 seconds and the other is an
|
|
||||||
* emergency trigger.
|
|
||||||
*/
|
|
||||||
public class LRRPPacketTest
|
|
||||||
{
|
|
||||||
//Periodic profile: ID FFFFEE, periodic trigger with a 600 second interval (0x31 0x84 0x58)
|
|
||||||
private static final String PERIODIC_REQUEST = "09112203FFFFEE524464429C10625734318458";
|
|
||||||
//Condition profile: ID FFFF02, trigger condition emergency (0x4A 0x02)
|
|
||||||
private static final String EMERGENCY_REQUEST = "09102203FFFF02524464429C106257344A02";
|
|
||||||
//PERIODIC_REQUEST as received with the packet type byte corrupted to 0xA3 (unknown type)
|
|
||||||
private static final String UNKNOWN_TYPE_REQUEST = "A3112203FFFFEE524464429C10625734318458";
|
|
||||||
//EMERGENCY_REQUEST with the packet type byte corrupted to 0x07 (immediate location response)
|
|
||||||
private static final String DAMAGED_TYPE_REQUEST = "07102203FFFF02524464429C106257344A02";
|
|
||||||
|
|
||||||
private static final List<TokenType> REQUESTED_FIELDS = List.of(TokenType.TIME, TokenType.HORIZONTAL_SPEED,
|
|
||||||
TokenType.HORIZONTAL_DIRECTION);
|
|
||||||
|
|
||||||
private static LRRPPacket parse(String hex)
|
|
||||||
{
|
|
||||||
return new LRRPPacket(CorrectedBinaryMessage.loadHex(hex), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T extends Token> T getToken(LRRPPacket packet, Class<T> clazz, TokenType tokenType)
|
|
||||||
{
|
|
||||||
for(Token token: packet.getTokens())
|
|
||||||
{
|
|
||||||
if(token.getTokenType() == tokenType && clazz.isInstance(token))
|
|
||||||
{
|
|
||||||
return clazz.cast(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail("Packet does not contain token " + tokenType + ": " + packet);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasToken(LRRPPacket packet, TokenType tokenType)
|
|
||||||
{
|
|
||||||
for(Token token: packet.getTokens())
|
|
||||||
{
|
|
||||||
if(token.getTokenType() == tokenType)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of payload bytes accounted for by the parsed tokens: parameter tokens contribute their byte length and
|
|
||||||
* each requested field flag contributes one byte.
|
|
||||||
*/
|
|
||||||
private static int getConsumedBytes(LRRPPacket packet)
|
|
||||||
{
|
|
||||||
int consumed = 0;
|
|
||||||
|
|
||||||
for(Token token: packet.getTokens())
|
|
||||||
{
|
|
||||||
if(token instanceof RequestedTokens requested)
|
|
||||||
{
|
|
||||||
consumed += requested.getRequestedTokens().size() + requested.getUnrecognizedRequestedTokens().size();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
consumed += token.getByteLength();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts the fields that both sample requests share.
|
|
||||||
*/
|
|
||||||
private static void assertCommonRequestTokens(LRRPPacket packet, int expectedId)
|
|
||||||
{
|
|
||||||
Identity identity = getToken(packet, Identity.class, TokenType.IDENTITY);
|
|
||||||
assertEquals(expectedId, identity.getID());
|
|
||||||
assertEquals(5, identity.getByteLength());
|
|
||||||
|
|
||||||
assertNotNull(getToken(packet, TriggerPeriodic.class, TokenType.TRIGGER_PERIODIC));
|
|
||||||
|
|
||||||
RequestParameter parameter42 = getToken(packet, RequestParameter.class, TokenType.REQUEST_PARAMETER_42);
|
|
||||||
assertEquals(3600, parameter42.getValue()); //0x42 0x9C 0x10
|
|
||||||
assertEquals(3, parameter42.getByteLength());
|
|
||||||
|
|
||||||
RequestParameter parameter44 = getToken(packet, RequestParameter.class, TokenType.REQUEST_PARAMETER_44);
|
|
||||||
assertEquals(100, parameter44.getValue()); //0x44 0x64
|
|
||||||
assertEquals(2, parameter44.getByteLength());
|
|
||||||
|
|
||||||
RequestedTokens requested = getToken(packet, RequestedTokens.class, TokenType.REQUESTED_TOKENS);
|
|
||||||
assertEquals(REQUESTED_FIELDS, requested.getRequestedTokens());
|
|
||||||
assertTrue(requested.getUnrecognizedRequestedTokens().isEmpty(), "No stray tokens: " + packet);
|
|
||||||
|
|
||||||
//Every payload byte is accounted for exactly once
|
|
||||||
assertEquals(packet.getHeader().getPayloadLength(), getConsumedBytes(packet));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void periodicTriggeredLocationStartRequest()
|
|
||||||
{
|
|
||||||
LRRPPacket packet = parse(PERIODIC_REQUEST);
|
|
||||||
|
|
||||||
assertEquals(LRRPPacketType.TRIGGERED_LOCATION_START_REQUEST, packet.getHeader().getLRRPPacketType());
|
|
||||||
assertEquals(17, packet.getHeader().getPayloadLength());
|
|
||||||
assertCommonRequestTokens(packet, 0xFFFFEE);
|
|
||||||
|
|
||||||
TriggerInterval interval = getToken(packet, TriggerInterval.class, TokenType.TRIGGER_INTERVAL);
|
|
||||||
assertEquals(600, interval.getSeconds()); //0x31 0x84 0x58
|
|
||||||
assertEquals(3, interval.getByteLength());
|
|
||||||
assertFalse(hasToken(packet, TokenType.TRIGGER_CONDITION));
|
|
||||||
|
|
||||||
assertEquals("LRRP TRIGGERED LOCATION START REQUEST ID:FFFFEE PERIODIC INTERVAL:600 SECS PARAM-42:3600 " +
|
|
||||||
"PARAM-44:100 REQUESTED TOKENS [TIME,HORIZONTAL SPEED,HORIZONTAL DIRECTION]", packet.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void emergencyTriggeredLocationStartRequest()
|
|
||||||
{
|
|
||||||
LRRPPacket packet = parse(EMERGENCY_REQUEST);
|
|
||||||
|
|
||||||
assertEquals(LRRPPacketType.TRIGGERED_LOCATION_START_REQUEST, packet.getHeader().getLRRPPacketType());
|
|
||||||
assertEquals(16, packet.getHeader().getPayloadLength());
|
|
||||||
assertCommonRequestTokens(packet, 0xFFFF02);
|
|
||||||
|
|
||||||
TriggerCondition condition = getToken(packet, TriggerCondition.class, TokenType.TRIGGER_CONDITION);
|
|
||||||
assertEquals(TriggerCondition.Condition.EMERGENCY, condition.getCondition()); //0x4A 0x02
|
|
||||||
assertEquals(2, condition.getByteLength());
|
|
||||||
assertFalse(hasToken(packet, TokenType.TRIGGER_INTERVAL));
|
|
||||||
|
|
||||||
assertEquals("LRRP TRIGGERED LOCATION START REQUEST ID:FFFF02 PERIODIC TRIGGER CONDITION:EMERGENCY " +
|
|
||||||
"PARAM-42:3600 PARAM-44:100 REQUESTED TOKENS [TIME,HORIZONTAL SPEED,HORIZONTAL DIRECTION]",
|
|
||||||
packet.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A corrupted type byte that is not a known packet type takes the request parsing path, so the intact body still
|
|
||||||
* decodes completely.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void unknownTypeRequestBodyStillDecodes()
|
|
||||||
{
|
|
||||||
LRRPPacket packet = parse(UNKNOWN_TYPE_REQUEST);
|
|
||||||
|
|
||||||
assertEquals(LRRPPacketType.UNKNOWN, packet.getHeader().getLRRPPacketType());
|
|
||||||
assertEquals(0xA3, packet.getHeader().getLrrpPacketTypeValue());
|
|
||||||
assertCommonRequestTokens(packet, 0xFFFFEE);
|
|
||||||
assertEquals(600, getToken(packet, TriggerInterval.class, TokenType.TRIGGER_INTERVAL).getSeconds());
|
|
||||||
|
|
||||||
assertEquals("LRRP TYPE:UNKNOWN [163] ID:FFFFEE PERIODIC INTERVAL:600 SECS PARAM-42:3600 PARAM-44:100 " +
|
|
||||||
"REQUESTED TOKENS [TIME,HORIZONTAL SPEED,HORIZONTAL DIRECTION]", packet.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type byte corrupted to a valid response type (0x07) sends the body down the response parsing path, where
|
|
||||||
* 0x34 is a timestamp rather than the periodic trigger flag. The parse must still terminate within the payload
|
|
||||||
* and the identity and variable length integer parameters must still decode.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void damagedTypeRequestParsesAsResponseWithoutError()
|
|
||||||
{
|
|
||||||
LRRPPacket packet = parse(DAMAGED_TYPE_REQUEST);
|
|
||||||
|
|
||||||
assertEquals(LRRPPacketType.IMMEDIATE_LOCATION_RESPONSE, packet.getHeader().getLRRPPacketType());
|
|
||||||
assertDoesNotThrow(packet::toString);
|
|
||||||
|
|
||||||
assertEquals(0xFFFF02, getToken(packet, Identity.class, TokenType.IDENTITY).getID());
|
|
||||||
assertEquals(3600, getToken(packet, RequestParameter.class, TokenType.REQUEST_PARAMETER_42).getValue());
|
|
||||||
assertEquals(100, getToken(packet, RequestParameter.class, TokenType.REQUEST_PARAMETER_44).getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JUnit tests for the LRRP 7-bit variable length integer reader.
|
|
||||||
*/
|
|
||||||
public class VarIntTest
|
|
||||||
{
|
|
||||||
private static CorrectedBinaryMessage hex(String hex)
|
|
||||||
{
|
|
||||||
return CorrectedBinaryMessage.loadHex(hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void singleByteValue()
|
|
||||||
{
|
|
||||||
CorrectedBinaryMessage message = hex("64");
|
|
||||||
assertEquals(1, VarInt.length(message, 0, 10));
|
|
||||||
assertEquals(100, VarInt.value(message, 0, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void twoByteValues()
|
|
||||||
{
|
|
||||||
//Values captured from Motorola triggered location start requests (interval 600 s, unknown parameter 3600)
|
|
||||||
assertEquals(2, VarInt.length(hex("8458"), 0, 10));
|
|
||||||
assertEquals(600, VarInt.value(hex("8458"), 0, 10));
|
|
||||||
assertEquals(2, VarInt.length(hex("9C10"), 0, 10));
|
|
||||||
assertEquals(3600, VarInt.value(hex("9C10"), 0, 10));
|
|
||||||
|
|
||||||
//Values from the TRBONet captures quoted in node-dmr-lib LRRP.js: interval 250 s and distance 500 m
|
|
||||||
assertEquals(250, VarInt.value(hex("817A"), 0, 10));
|
|
||||||
assertEquals(500, VarInt.value(hex("8374"), 0, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void valueAtOffset()
|
|
||||||
{
|
|
||||||
//Token identifier 0x31 followed by the value - read starting at bit 8
|
|
||||||
CorrectedBinaryMessage message = hex("318458");
|
|
||||||
assertEquals(2, VarInt.length(message, 8, 2));
|
|
||||||
assertEquals(600, VarInt.value(message, 8, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void limitBoundsTheRead()
|
|
||||||
{
|
|
||||||
CorrectedBinaryMessage message = hex("8458");
|
|
||||||
|
|
||||||
//Only one byte available: the continuation bit is set but the read stops at the limit
|
|
||||||
assertEquals(1, VarInt.length(message, 0, 1));
|
|
||||||
assertEquals(4, VarInt.value(message, 0, 1));
|
|
||||||
|
|
||||||
//No bytes available
|
|
||||||
assertEquals(0, VarInt.length(message, 0, 0));
|
|
||||||
assertEquals(0, VarInt.value(message, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maximumLength()
|
|
||||||
{
|
|
||||||
//A run of continuation bits longer than MAX_BYTES stops at MAX_BYTES
|
|
||||||
CorrectedBinaryMessage message = hex("FFFFFFFFFFFFFF");
|
|
||||||
assertEquals(VarInt.MAX_BYTES, VarInt.length(message, 0, 10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
/*
|
|
||||||
* *****************************************************************************
|
|
||||||
* 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
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import io.github.dsheirer.bits.BinaryMessage;
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|
||||||
import io.github.dsheirer.module.decode.ip.IPacket;
|
|
||||||
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
|
|
||||||
import io.github.dsheirer.module.decode.ip.mototrbo.ars.RegistrationAcknowledgement;
|
|
||||||
import io.github.dsheirer.module.decode.ip.udp.UDPPacket;
|
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
|
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.block.DataBlock;
|
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketHeader;
|
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketMessage;
|
|
||||||
import io.github.dsheirer.module.decode.p25.reference.PDUFormat;
|
|
||||||
import io.github.dsheirer.module.decode.p25.reference.PDUType;
|
|
||||||
import io.github.dsheirer.module.decode.p25.reference.ServiceAccessPoint;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for PDU header and message creation, in particular that an unconfirmed data header (format 0x15) that
|
|
||||||
* carries a packet data SAP is processed as an IP packet and not as unconfirmed multi-block trunking control.
|
|
||||||
*/
|
|
||||||
public class PDUMessageFactoryTest
|
|
||||||
{
|
|
||||||
private static final int NAC = 0x460;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Outbound unconfirmed data header: format 0x15, SAP 4 (PACKET DATA), MFID 0, LLID 4006527, 4 blocks to
|
|
||||||
* follow, 10 pad octets, data header offset 2. The header CRC octets are zero; the header factory derives
|
|
||||||
* the CRC result from the corrected bit count and does not evaluate them.
|
|
||||||
*/
|
|
||||||
private static final String UNCONFIRMED_SAP_4_HEADER = "3504003D227F840A00020000";
|
|
||||||
private static final String UNCONFIRMED_SAP_0_HEADER = "3500003D227F840A00020000";
|
|
||||||
private static final String UNCONFIRMED_SAP_6_HEADER = "3506003D227F840A00020000";
|
|
||||||
private static final String UNCONFIRMED_SAP_61_HEADER = "353D003D227F840A00020000";
|
|
||||||
private static final String UNCONFIRMED_SAP_63_HEADER = "353F003D227F840A00020000";
|
|
||||||
/** Same as UNCONFIRMED_SAP_4_HEADER with LLID 4001080 and 5 pad octets */
|
|
||||||
private static final String UNCONFIRMED_SAP_4_HEADER_PAD_5 = "3504003D0D38840500020000";
|
|
||||||
/** Outbound confirmed data header: format 0x16, SAP 0, LLID 4006527, 4 blocks, 10 pad octets, offset 2 */
|
|
||||||
private static final String CONFIRMED_SAP_0_HEADER = "7600003D227F840A00020000";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unconfirmed (1/2 rate, 12 octet) data blocks captured from a Motorola P25 Phase 1 control channel, as logged
|
|
||||||
* by the PDUSequence MSG: field. SNDCP header 0x4100 (RF UNCONFIRMED DATA, NSAPI 1), IPv4/UDP from
|
|
||||||
* 10.51.4.116:49516 to 10.71.213.29:4005, ARS registration acknowledgement (refresh 8 x 30 minutes),
|
|
||||||
* 10 pad octets (0xAA) and the 32-bit packet CRC.
|
|
||||||
*/
|
|
||||||
private static final String[] ARS_ACK_BLOCKS = {"410045000020280200008011", "24C00A3304740A47D51DC16C",
|
|
||||||
"0FA5000C81AE0002BF08AAAA", "AAAAAAAAAAAAAAAAC0F19102"};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As above, to 10.71.212.139 with a 5 octet trailer (0x04 + 32-bit time) after the ARS extension octet and
|
|
||||||
* 5 pad octets.
|
|
||||||
*/
|
|
||||||
private static final String[] ARS_ACK_WITH_TRAILER_BLOCKS = {"4100450000255F7E00008011", "EDD00A3304740A47D48BC16C",
|
|
||||||
"0FA5001113E60007BF08046A", "D3E096AAAAAAAAAA8675BA00"};
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unconfirmedDataHeaderWithPacketDataSapIsPacketHeader()
|
|
||||||
{
|
|
||||||
for(String hex : new String[]{UNCONFIRMED_SAP_0_HEADER, UNCONFIRMED_SAP_4_HEADER, UNCONFIRMED_SAP_6_HEADER})
|
|
||||||
{
|
|
||||||
PDUHeader header = PDUHeaderFactory.getPDUHeader(CorrectedBinaryMessage.loadHex(hex));
|
|
||||||
assertEquals(PDUFormat.UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL, header.getFormat(), hex);
|
|
||||||
assertInstanceOf(PacketHeader.class, header, hex);
|
|
||||||
assertFalse(header.isConfirmationRequired(), hex);
|
|
||||||
assertTrue(header.isOutbound(), hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
PacketHeader header = (PacketHeader)PDUHeaderFactory.getPDUHeader(CorrectedBinaryMessage.loadHex(UNCONFIRMED_SAP_4_HEADER));
|
|
||||||
assertEquals(ServiceAccessPoint.PACKET_DATA, header.getServiceAccessPoint());
|
|
||||||
assertEquals(4, header.getBlocksToFollowCount());
|
|
||||||
assertEquals(10, header.getPadOctetCount());
|
|
||||||
assertEquals(2, header.getDataHeaderOffset());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unconfirmedDataHeaderWithTrunkingControlSapIsNotPacketHeader()
|
|
||||||
{
|
|
||||||
for(String hex : new String[]{UNCONFIRMED_SAP_61_HEADER, UNCONFIRMED_SAP_63_HEADER})
|
|
||||||
{
|
|
||||||
PDUHeader header = PDUHeaderFactory.getPDUHeader(CorrectedBinaryMessage.loadHex(hex));
|
|
||||||
assertEquals(PDUFormat.UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL, header.getFormat(), hex);
|
|
||||||
assertFalse(header instanceof PacketHeader, hex);
|
|
||||||
|
|
||||||
P25P1Message message = PDUMessageFactory.create(new PDUSequence(header, 0, NAC), NAC, 0);
|
|
||||||
assertInstanceOf(PDUSequenceMessage.class, message, hex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void confirmedPacketDataHeaderIsPacketHeader()
|
|
||||||
{
|
|
||||||
PDUHeader header = PDUHeaderFactory.getPDUHeader(CorrectedBinaryMessage.loadHex(CONFIRMED_SAP_0_HEADER));
|
|
||||||
assertEquals(PDUFormat.PACKET_DATA, header.getFormat());
|
|
||||||
assertInstanceOf(PacketHeader.class, header);
|
|
||||||
assertTrue(header.isConfirmationRequired());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unconfirmedPacketDataDecodesAsIpPacket()
|
|
||||||
{
|
|
||||||
PacketMessage packetMessage = createPacketMessage(UNCONFIRMED_SAP_4_HEADER, ARS_ACK_BLOCKS);
|
|
||||||
|
|
||||||
assertEquals(PDUType.OUTBOUND_SNDCP_RF_UNCONFIRMED_DATA, packetMessage.getSNDCPPacketHeader().getPDUType());
|
|
||||||
assertEquals(1, packetMessage.getSNDCPPacketHeader().getNSAPI());
|
|
||||||
//2 octets SNDCP header + 32 octet IP packet + 10 pad octets + 4 CRC octets = 48 octets = 4 x 12 octet blocks
|
|
||||||
assertEquals(32 * 8, packetMessage.getPacketMessage().size());
|
|
||||||
|
|
||||||
RegistrationAcknowledgement acknowledgement = getRegistrationAcknowledgement(packetMessage);
|
|
||||||
assertTrue(acknowledgement.isValid());
|
|
||||||
assertTrue(acknowledgement.isSuccessful());
|
|
||||||
assertEquals(8, acknowledgement.getPayloadValue());
|
|
||||||
|
|
||||||
String text = packetMessage.toString();
|
|
||||||
assertTrue(text.contains(" IPPKT "), text);
|
|
||||||
assertTrue(text.contains("LLID:4006527"), text);
|
|
||||||
assertTrue(text.contains("NSAPI:1"), text);
|
|
||||||
assertTrue(text.contains("IP FROM:10.51.4.116 TO:10.71.213.29"), text);
|
|
||||||
assertTrue(text.contains("UDP PORT FROM:49516 TO:4005"), text);
|
|
||||||
assertTrue(text.contains("ARS REGISTRATION SUCCESS REFRESH IN:240mins"), text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unconfirmedPacketDataWithArsTrailerDecodesAsIpPacket()
|
|
||||||
{
|
|
||||||
PacketMessage packetMessage = createPacketMessage(UNCONFIRMED_SAP_4_HEADER_PAD_5, ARS_ACK_WITH_TRAILER_BLOCKS);
|
|
||||||
|
|
||||||
//2 octets SNDCP header + 37 octet IP packet + 5 pad octets + 4 CRC octets = 48 octets
|
|
||||||
assertEquals(37 * 8, packetMessage.getPacketMessage().size());
|
|
||||||
|
|
||||||
RegistrationAcknowledgement acknowledgement = getRegistrationAcknowledgement(packetMessage);
|
|
||||||
assertTrue(acknowledgement.isValid());
|
|
||||||
assertTrue(acknowledgement.isSuccessful());
|
|
||||||
assertEquals(8, acknowledgement.getPayloadValue());
|
|
||||||
|
|
||||||
String text = packetMessage.toString();
|
|
||||||
assertTrue(text.contains("LLID:4001080"), text);
|
|
||||||
assertTrue(text.contains("IP FROM:10.51.4.116 TO:10.71.212.139"), text);
|
|
||||||
assertTrue(text.contains("UDP PORT FROM:49516 TO:4005"), text);
|
|
||||||
assertTrue(text.contains("ARS REGISTRATION SUCCESS REFRESH IN:240mins"), text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a PDU sequence from the header and data blocks and processes it with the PDU message factory.
|
|
||||||
*/
|
|
||||||
private static PacketMessage createPacketMessage(String headerHex, String[] blocksHex)
|
|
||||||
{
|
|
||||||
PDUHeader header = PDUHeaderFactory.getPDUHeader(CorrectedBinaryMessage.loadHex(headerHex));
|
|
||||||
PDUSequence sequence = new PDUSequence(header, 0, NAC);
|
|
||||||
|
|
||||||
for(String blockHex : blocksHex)
|
|
||||||
{
|
|
||||||
sequence.addDataBlock(dataBlock(blockHex));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(sequence.isComplete());
|
|
||||||
|
|
||||||
P25P1Message message = PDUMessageFactory.create(sequence, NAC, 0);
|
|
||||||
assertInstanceOf(PacketMessage.class, message);
|
|
||||||
return (PacketMessage)message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RegistrationAcknowledgement getRegistrationAcknowledgement(PacketMessage packetMessage)
|
|
||||||
{
|
|
||||||
IPacket packet = packetMessage.getPacket();
|
|
||||||
assertInstanceOf(IPV4Packet.class, packet);
|
|
||||||
IPacket udp = ((IPV4Packet)packet).getPayload();
|
|
||||||
assertInstanceOf(UDPPacket.class, udp);
|
|
||||||
assertEquals(4005, ((UDPPacket)udp).getHeader().getDestinationPort().getValue());
|
|
||||||
IPacket ars = ((UDPPacket)udp).getPayload();
|
|
||||||
assertInstanceOf(ARSPacket.class, ars);
|
|
||||||
assertInstanceOf(RegistrationAcknowledgement.class, ((ARSPacket)ars).getHeader());
|
|
||||||
return (RegistrationAcknowledgement)((ARSPacket)ars).getHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data block that returns an already decoded (1/2 rate, 96-bit) payload.
|
|
||||||
*/
|
|
||||||
private static DataBlock dataBlock(String hex)
|
|
||||||
{
|
|
||||||
BinaryMessage message = BinaryMessage.loadHex(hex);
|
|
||||||
|
|
||||||
return new DataBlock()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public BinaryMessage getMessage()
|
|
||||||
{
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getBitErrorsCount()
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValid()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.signalhound;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.github.dsheirer.buffer.INativeBuffer;
|
||||||
|
import io.github.dsheirer.sample.Listener;
|
||||||
|
import io.github.dsheirer.signalhound.DeviceDescriptor;
|
||||||
|
import io.github.dsheirer.signalhound.Diagnostics;
|
||||||
|
import io.github.dsheirer.signalhound.IqBlock;
|
||||||
|
import io.github.dsheirer.signalhound.IqBuffer;
|
||||||
|
import io.github.dsheirer.signalhound.IqConfig;
|
||||||
|
import io.github.dsheirer.signalhound.IqDataType;
|
||||||
|
import io.github.dsheirer.signalhound.IqStreamInfo;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundApi;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundDevice;
|
||||||
|
import io.github.dsheirer.signalhound.SignalHoundException;
|
||||||
|
import io.github.dsheirer.signalhound.Sweep;
|
||||||
|
import io.github.dsheirer.signalhound.SweepConfig;
|
||||||
|
import io.github.dsheirer.signalhound.SweepInfo;
|
||||||
|
import io.github.dsheirer.source.SourceException;
|
||||||
|
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerFactory;
|
||||||
|
import io.github.dsheirer.source.tuner.TunerType;
|
||||||
|
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||||
|
import java.lang.foreign.ValueLayout;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the Signal Hound tuner, using a simulated device since the tests can't assume Signal Hound hardware.
|
||||||
|
*/
|
||||||
|
class SignalHoundTunerTest
|
||||||
|
{
|
||||||
|
private static final DeviceDescriptor SM200 = new DeviceDescriptor(SignalHoundApi.SM, 1234, "SM200B");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sampleRatesAreTheBaseRateDecimatedByPowersOfTwo()
|
||||||
|
{
|
||||||
|
List<SignalHoundSampleRate> bb60 = SignalHoundSampleRate.getRates(SignalHoundFamily.BB60);
|
||||||
|
assertEquals(new SignalHoundSampleRate(1, 40_000_000), bb60.getFirst());
|
||||||
|
assertEquals(new SignalHoundSampleRate(64, 625_000), bb60.getLast());
|
||||||
|
assertEquals(new SignalHoundSampleRate(64, 781_250), SignalHoundSampleRate.getRates(SignalHoundFamily.SM).getLast());
|
||||||
|
assertEquals(new SignalHoundSampleRate(64, 960_000), SignalHoundSampleRate.getRates(SignalHoundFamily.SP145).getLast());
|
||||||
|
|
||||||
|
for(SignalHoundFamily family : SignalHoundFamily.values())
|
||||||
|
{
|
||||||
|
for(SignalHoundSampleRate rate : SignalHoundSampleRate.getRates(family))
|
||||||
|
{
|
||||||
|
assertEquals(family.getBaseSampleRate(), (long)rate.rate() * rate.decimation(), "Rate " + rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closestRateDoesNotExceedTheRequestedRate()
|
||||||
|
{
|
||||||
|
List<SignalHoundSampleRate> bb60 = SignalHoundSampleRate.getRates(SignalHoundFamily.BB60);
|
||||||
|
assertEquals(10_000_000, SignalHoundSampleRate.getClosest(bb60, 10_000_000).rate());
|
||||||
|
assertEquals(6_250_000, SignalHoundSampleRate.getClosest(SignalHoundSampleRate.getRates(SignalHoundFamily.SM),
|
||||||
|
10_000_000).rate());
|
||||||
|
assertEquals(7_680_000, SignalHoundSampleRate.getClosest(
|
||||||
|
SignalHoundSampleRate.getRates(SignalHoundFamily.SP145), 10_000_000).rate());
|
||||||
|
assertEquals(625_000, SignalHoundSampleRate.getClosest(bb60, 1).rate());
|
||||||
|
assertEquals("1.5625 MHz", new SignalHoundSampleRate(32, 1_562_500).toString());
|
||||||
|
assertEquals("10.00 MHz", new SignalHoundSampleRate(4, 10_000_000).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void familyLimits()
|
||||||
|
{
|
||||||
|
assertEquals(SignalHoundFamily.SM, SignalHoundFamily.of(SignalHoundApi.SM));
|
||||||
|
assertEquals(20_600_000_000L, SignalHoundFamily.SM.getMaximumFrequency("SM200B"));
|
||||||
|
assertEquals(44_200_000_000L, SignalHoundFamily.SM.getMaximumFrequency("SM435C"));
|
||||||
|
assertEquals(6_400_000_000L, SignalHoundFamily.BB60.getMaximumFrequency("BB60D"));
|
||||||
|
assertTrue(SignalHoundFamily.BB60.hasGain());
|
||||||
|
assertFalse(SignalHoundFamily.SM.hasGain());
|
||||||
|
assertEquals(SignalHoundTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ,
|
||||||
|
SignalHoundFamily.SM.getMaximumFrequency("SM435B"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsAreAbout10MillisecondsAsAPowerOfTwo()
|
||||||
|
{
|
||||||
|
assertEquals(65536, SignalHoundTunerController.getBufferSampleCount(10_000_000));
|
||||||
|
assertEquals(8192, SignalHoundTunerController.getBufferSampleCount(625_000));
|
||||||
|
assertEquals(524288, SignalHoundTunerController.getBufferSampleCount(61_440_000));
|
||||||
|
assertEquals(8192, SignalHoundTunerController.getBufferSampleCount(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void referenceLevelIsFullScale()
|
||||||
|
{
|
||||||
|
//A -20 dBm signal has an amplitude of sqrt(0.01 mW) = 0.1
|
||||||
|
assertEquals(10.0f, SignalHoundTunerController.getSampleScale(-20), 1e-5f);
|
||||||
|
assertEquals(1.0f, SignalHoundTunerController.getSampleScale(0), 1e-6f);
|
||||||
|
assertEquals(0.1f, SignalHoundTunerController.getSampleScale(20), 1e-6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usableBandwidthComesFromTheStreamBandwidth()
|
||||||
|
{
|
||||||
|
assertEquals(0.75, SignalHoundTunerController.getUsableBandwidthPercent(
|
||||||
|
new IqStreamInfo(40e6, 30e6, IqDataType.FLOAT32)), 1e-9);
|
||||||
|
assertEquals(0.9, SignalHoundTunerController.getUsableBandwidthPercent(
|
||||||
|
new IqStreamInfo(10e6, 9.9e6, IqDataType.FLOAT32)), 1e-9);
|
||||||
|
assertEquals(0.5, SignalHoundTunerController.getUsableBandwidthPercent(
|
||||||
|
new IqStreamInfo(10e6, 1e6, IqDataType.FLOAT32)), 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidSettingsAreAutomatic()
|
||||||
|
{
|
||||||
|
assertEquals(3, SignalHoundTunerController.getValidSetting(3, 3));
|
||||||
|
assertEquals(SignalHoundTunerController.AUTOMATIC, SignalHoundTunerController.getValidSetting(4, 3));
|
||||||
|
assertEquals(SignalHoundTunerController.AUTOMATIC, SignalHoundTunerController.getValidSetting(-5, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idUsesTheFamilyAndSerialNumber()
|
||||||
|
{
|
||||||
|
assertEquals("Signal Hound SM SER#1234", DiscoveredSignalHoundTuner.getId(SM200));
|
||||||
|
assertEquals("Signal Hound SP145 SER#99", DiscoveredSignalHoundTuner.getId(
|
||||||
|
new DeviceDescriptor(SignalHoundApi.SP, 99, "SP145")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void configurationRoundTrips() throws Exception
|
||||||
|
{
|
||||||
|
SignalHoundTunerConfiguration configuration = new SignalHoundTunerConfiguration("test");
|
||||||
|
configuration.setSampleRate(5_000_000);
|
||||||
|
configuration.setReferenceLevel(-40);
|
||||||
|
configuration.setAttenuation(2);
|
||||||
|
configuration.setGain(1);
|
||||||
|
configuration.setPreselector(true);
|
||||||
|
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
TunerConfiguration restored = mapper.readValue(mapper.writeValueAsString(configuration),
|
||||||
|
TunerConfiguration.class);
|
||||||
|
|
||||||
|
SignalHoundTunerConfiguration signalHound = assertInstanceOf(SignalHoundTunerConfiguration.class, restored);
|
||||||
|
assertEquals(TunerType.SIGNAL_HOUND, signalHound.getTunerType());
|
||||||
|
assertEquals(5_000_000, signalHound.getSampleRate());
|
||||||
|
assertEquals(-40, signalHound.getReferenceLevel());
|
||||||
|
assertEquals(2, signalHound.getAttenuation());
|
||||||
|
assertEquals(1, signalHound.getGain());
|
||||||
|
assertTrue(signalHound.isPreselector());
|
||||||
|
assertEquals("test", signalHound.getUniqueID());
|
||||||
|
assertInstanceOf(SignalHoundTunerConfiguration.class,
|
||||||
|
TunerFactory.getTunerConfiguration(TunerType.SIGNAL_HOUND, "id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streamsScaledSamplesAndReconfiguresForChanges() throws Exception
|
||||||
|
{
|
||||||
|
FakeDevice device = new FakeDevice(0.01f);
|
||||||
|
ErrorListener errors = new ErrorListener();
|
||||||
|
SignalHoundTunerController controller = new SignalHoundTunerController(SM200, errors, _ -> device);
|
||||||
|
controller.start();
|
||||||
|
|
||||||
|
assertEquals("SM200B", controller.getDeviceInfo().model());
|
||||||
|
assertEquals(20_600_000_000L, controller.getMaximumTunableFrequency());
|
||||||
|
assertEquals(6_250_000, controller.getSignalHoundSampleRate().rate());
|
||||||
|
assertFalse(controller.isPreselectorSupported(), "Only SmDevice and SP145B have a preselector");
|
||||||
|
assertTrue(device.configs.isEmpty(), "The stream isn't configured until there's a listener");
|
||||||
|
|
||||||
|
//Small, since each buffer is up to 512 KB. Buffers are dropped while it is full.
|
||||||
|
BlockingQueue<INativeBuffer> buffers = new ArrayBlockingQueue<>(16);
|
||||||
|
Listener<INativeBuffer> listener = buffers::offer;
|
||||||
|
controller.addBufferListener(listener);
|
||||||
|
|
||||||
|
//0.01 sqrt(mW) at the default -20 dBm reference level is 0.1 of full scale
|
||||||
|
assertEquals(0.1f, firstSample(buffers), 1e-6f);
|
||||||
|
IqConfig first = device.configs.getLast();
|
||||||
|
assertEquals(8, first.decimation());
|
||||||
|
assertEquals(-20.0, first.referenceLevel());
|
||||||
|
assertEquals(IqDataType.FLOAT32, first.dataType());
|
||||||
|
|
||||||
|
controller.setFrequency(460_000_000L);
|
||||||
|
assertEquals(460_000_000.0, device.configs.getLast().centerFrequency());
|
||||||
|
|
||||||
|
controller.setReferenceLevel(-40);
|
||||||
|
assertEquals(-40.0, device.configs.getLast().referenceLevel());
|
||||||
|
//A block read before the change can still be in flight to the listeners
|
||||||
|
awaitSample(buffers, 1.0f);
|
||||||
|
|
||||||
|
controller.setSampleRate(controller.getSampleRates().get(2));
|
||||||
|
assertEquals(4, device.configs.getLast().decimation());
|
||||||
|
assertEquals(12_500_000.0, controller.getSampleRate());
|
||||||
|
awaitSampleCount(buffers, 65536);
|
||||||
|
|
||||||
|
controller.removeBufferListener(listener);
|
||||||
|
assertFalse(device.streaming, "The stream stops without listeners");
|
||||||
|
assertTrue(device.stops > 0);
|
||||||
|
|
||||||
|
controller.stop();
|
||||||
|
assertTrue(device.closed);
|
||||||
|
assertTrue(errors.messages.isEmpty(), "Errors: " + errors.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settingsAreAppliedWhenTheStreamStarts() throws Exception
|
||||||
|
{
|
||||||
|
FakeDevice device = new FakeDevice(0.01f);
|
||||||
|
SignalHoundTunerController controller = new SignalHoundTunerController(SM200, new ErrorListener(),
|
||||||
|
_ -> device);
|
||||||
|
controller.start();
|
||||||
|
|
||||||
|
SignalHoundTunerConfiguration configuration = new SignalHoundTunerConfiguration("id");
|
||||||
|
configuration.setFrequency(851_000_000L);
|
||||||
|
configuration.setSampleRate(25_000_000);
|
||||||
|
configuration.setReferenceLevel(-100);
|
||||||
|
configuration.setAttenuation(9);
|
||||||
|
configuration.setGain(2);
|
||||||
|
configuration.setPreselector(true);
|
||||||
|
controller.apply(configuration);
|
||||||
|
|
||||||
|
//Settings that the device doesn't support are corrected in the configuration
|
||||||
|
assertEquals(SignalHoundTunerController.REFERENCE_LEVEL_MINIMUM, configuration.getReferenceLevel());
|
||||||
|
assertEquals(SignalHoundTunerController.AUTOMATIC, configuration.getAttenuation());
|
||||||
|
assertEquals(SignalHoundTunerController.AUTOMATIC, configuration.getGain());
|
||||||
|
assertFalse(configuration.isPreselector());
|
||||||
|
assertTrue(device.configs.isEmpty());
|
||||||
|
|
||||||
|
Listener<INativeBuffer> listener = _ -> {};
|
||||||
|
controller.addBufferListener(listener);
|
||||||
|
IqConfig config = device.configs.getLast();
|
||||||
|
assertEquals(851_000_000.0, config.centerFrequency());
|
||||||
|
assertEquals(2, config.decimation());
|
||||||
|
assertEquals(-60.0, config.referenceLevel());
|
||||||
|
assertEquals(1, device.configs.size(), "Configured once when the stream starts");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> controller.setGain(1), "The SM doesn't have gain");
|
||||||
|
assertThrows(SourceException.class, () -> controller.setPreselector(true));
|
||||||
|
controller.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readFailureStopsTheTunerWithAnError() throws Exception
|
||||||
|
{
|
||||||
|
FakeDevice device = new FakeDevice(0.01f);
|
||||||
|
ErrorListener errors = new ErrorListener();
|
||||||
|
SignalHoundTunerController controller = new SignalHoundTunerController(SM200, errors, _ -> device);
|
||||||
|
controller.start();
|
||||||
|
controller.addBufferListener(_ -> {});
|
||||||
|
|
||||||
|
device.readFailure = new SignalHoundException(SignalHoundApi.SM, "smGetIQ", -11, "Device disconnected");
|
||||||
|
String message = errors.messages.poll(5, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertNotNull(message);
|
||||||
|
assertTrue(message.contains("Device disconnected"), message);
|
||||||
|
controller.stop();
|
||||||
|
assertTrue(device.closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startFailureClosesTheDevice()
|
||||||
|
{
|
||||||
|
SignalHoundTunerController controller = new SignalHoundTunerController(SM200, new ErrorListener(), _ -> {
|
||||||
|
throw new SignalHoundException(SignalHoundApi.SM, "smOpenDeviceBySerial", -1, "Device not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
SourceException exception = assertThrows(SourceException.class, controller::start);
|
||||||
|
assertTrue(exception.getMessage().contains("Device not found"), exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a buffer and returns its first I sample.
|
||||||
|
*/
|
||||||
|
private static float firstSample(BlockingQueue<INativeBuffer> buffers) throws InterruptedException
|
||||||
|
{
|
||||||
|
INativeBuffer buffer = buffers.poll(5, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(buffer, "No samples received");
|
||||||
|
return buffer.iteratorInterleaved().next().samples()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a buffer whose first I sample is the expected value.
|
||||||
|
*/
|
||||||
|
private static void awaitSample(BlockingQueue<INativeBuffer> buffers, float expected) throws InterruptedException
|
||||||
|
{
|
||||||
|
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||||
|
float sample = Float.NaN;
|
||||||
|
|
||||||
|
while(System.nanoTime() < end)
|
||||||
|
{
|
||||||
|
sample = firstSample(buffers);
|
||||||
|
|
||||||
|
if(Math.abs(sample - expected) < 1e-5f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, sample, 1e-5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a buffer with the expected sample count.
|
||||||
|
*/
|
||||||
|
private static void awaitSampleCount(BlockingQueue<INativeBuffer> buffers, int expected) throws InterruptedException
|
||||||
|
{
|
||||||
|
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while(System.nanoTime() < end)
|
||||||
|
{
|
||||||
|
INativeBuffer buffer = buffers.poll(5, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(buffer, "No samples received");
|
||||||
|
count = buffer.sampleCount();
|
||||||
|
|
||||||
|
if(count == expected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ErrorListener implements ITunerErrorListener
|
||||||
|
{
|
||||||
|
private final BlockingQueue<String> messages = new ArrayBlockingQueue<>(10);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setErrorMessage(String errorMessage)
|
||||||
|
{
|
||||||
|
messages.offer(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tunerRemoved()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulated SM200 that streams a constant sample value, reading as fast as the test reads.
|
||||||
|
*/
|
||||||
|
private static class FakeDevice implements SignalHoundDevice
|
||||||
|
{
|
||||||
|
private final float mValue;
|
||||||
|
private final List<IqConfig> configs = new CopyOnWriteArrayList<>();
|
||||||
|
private volatile boolean streaming;
|
||||||
|
private volatile boolean closed;
|
||||||
|
private volatile int stops;
|
||||||
|
private volatile SignalHoundException readFailure;
|
||||||
|
private volatile RuntimeException firmwareFailure;
|
||||||
|
|
||||||
|
FakeDevice(float value)
|
||||||
|
{
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignalHoundApi api()
|
||||||
|
{
|
||||||
|
return SignalHoundApi.SM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int serialNumber()
|
||||||
|
{
|
||||||
|
return 1234;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String model()
|
||||||
|
{
|
||||||
|
return "SM200B";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String firmwareVersion()
|
||||||
|
{
|
||||||
|
if(firmwareFailure != null)
|
||||||
|
{
|
||||||
|
throw firmwareFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "8.1.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Diagnostics diagnostics()
|
||||||
|
{
|
||||||
|
return new Diagnostics(40.0f, 5.0f, 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized IqStreamInfo startIq(IqConfig config)
|
||||||
|
{
|
||||||
|
configs.add(config);
|
||||||
|
streaming = true;
|
||||||
|
double rate = SignalHoundFamily.SM.getBaseSampleRate() / (double)config.decimation();
|
||||||
|
return new IqStreamInfo(rate, Math.min(config.bandwidth(), rate * 0.8), config.dataType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized IqBlock readIq(IqBuffer buffer, boolean purge) throws SignalHoundException
|
||||||
|
{
|
||||||
|
if(!streaming)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Not streaming");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(readFailure != null)
|
||||||
|
{
|
||||||
|
throw readFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.segment().fill((byte)0);
|
||||||
|
|
||||||
|
for(int x = 0; x < buffer.capacity() * 2; x++)
|
||||||
|
{
|
||||||
|
buffer.segment().setAtIndex(ValueLayout.JAVA_FLOAT, x, mValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(1);
|
||||||
|
}
|
||||||
|
catch(InterruptedException e)
|
||||||
|
{
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IqBlock(buffer.capacity(), System.currentTimeMillis() * 1_000_000L, false, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float iqCorrection()
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SweepInfo startSweep(SweepConfig config)
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sweep readSweep()
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void stop()
|
||||||
|
{
|
||||||
|
streaming = false;
|
||||||
|
stops++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean isClosed()
|
||||||
|
{
|
||||||
|
return closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close()
|
||||||
|
{
|
||||||
|
streaming = false;
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +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.util;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
public class DuplicateLibraryDetectorTest
|
|
||||||
{
|
|
||||||
private static final String LIB = "/opt/sdr-trunk/bin/../lib/";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jar names from a clean release image.
|
|
||||||
*/
|
|
||||||
private static final List<String> CLEAN = List.of("JLargeArrays-1.7.jar", "JTransforms-3.2.jar", "api-v2.1.1.jar",
|
|
||||||
"commons-lang3-3.21.0.jar", "guava-33.8.0-jre.jar", "jackson-annotations-2.23.jar",
|
|
||||||
"jackson-core-2.23.0.jar", "java-lame-v3.98.4.jar", "javafx-swt.jar", "jhackrf-2026.01.3.1.jar",
|
|
||||||
"jiconfont-1.0.0.jar", "jiconfont-font_awesome-4.7.0.1.jar", "jiconfont-javafx-1.0.0.jar",
|
|
||||||
"jiconfont-swing-1.0.1.jar", "jrt-fs.jar", "jrtl-1.4.0.3.jar", "libresample4j-master.jar",
|
|
||||||
"listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar", "logback-classic-1.6.6.jar",
|
|
||||||
"logback-core-1.6.6.jar", "sdr-trunk-nightly.jar");
|
|
||||||
|
|
||||||
private static List<String> paths(List<String> names)
|
|
||||||
{
|
|
||||||
return names.stream().map(n -> LIB + n).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void cleanReleaseHasNoDuplicates()
|
|
||||||
{
|
|
||||||
assertTrue(DuplicateLibraryDetector.findDuplicates(paths(CLEAN)).isEmpty());
|
|
||||||
assertNull(DuplicateLibraryDetector.getFolder(Map.of()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void newReleaseUnzippedOverOldReleaseIsDetected()
|
|
||||||
{
|
|
||||||
List<String> names = new java.util.ArrayList<>(CLEAN);
|
|
||||||
names.addAll(List.of("api-v2.1.0.jar", "guava-33.6.0-jre.jar", "jrtl-1.4.0.2.jar", "sdr-trunk-0.6.1.jar"));
|
|
||||||
|
|
||||||
Map<String,List<String>> duplicates = DuplicateLibraryDetector.findDuplicates(paths(names));
|
|
||||||
|
|
||||||
assertEquals(List.of("api", "guava", "jrtl", "sdr-trunk"), List.copyOf(duplicates.keySet()));
|
|
||||||
assertEquals(List.of(LIB + "jrtl-1.4.0.2.jar", LIB + "jrtl-1.4.0.3.jar"), duplicates.get("jrtl"));
|
|
||||||
assertTrue(DuplicateLibraryDetector.describe(duplicates).contains("jrtl: jrtl-1.4.0.2.jar, jrtl-1.4.0.3.jar"));
|
|
||||||
//Windows resolves a rooted path without a drive letter against the current drive, e.g. C:\opt\sdr-trunk\lib
|
|
||||||
assertEquals(Path.of("/opt/sdr-trunk/lib").toAbsolutePath(), DuplicateLibraryDetector.getFolder(duplicates));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void sameJarListedTwiceIsNotADuplicate()
|
|
||||||
{
|
|
||||||
assertTrue(DuplicateLibraryDetector.findDuplicates(List.of(LIB + "jrtl-1.4.0.3.jar", LIB + "jrtl-1.4.0.3.jar",
|
|
||||||
"", "/some/classes/dir")).isEmpty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user