Compare commits

...

23 Commits

Author SHA1 Message Date
Denny Sheirer
73d3238644 #2634 Gradle build now uses git tag for versioning (#2635)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-13 03:32:08 -05:00
farewell-ladmin
4626d56730 #2392 P25P1: trust HDU/LDU2 algorithm ID over LC service options encryption flag (#2477)
Some radios set the encryption flag in the service options of the LDU1 Group
Voice Channel User link control while transmitting clear voice; the HDU and
LDU2 encryption sync parameters report ALGID 0x80 (unencrypted) for the same
call. Example (CoMIRS Boston, TG 2217, valid LCs from several radios):

  HDU   TALKGROUP:2217 UNENCRYPTED
  LDU1  GROUP VOICE CHANNEL USER FM:64008 TO:2217 SERVICE OPTIONS:PRI4 ENCRYPTED CIRCUIT
  LDU2  UNENCRYPTED

P25P1DecoderState broadcast State.ENCRYPTED for each such LDU1 and State.CALL
for each LDU2, so the channel state flapped between CALL and ENCRYPTED (and on
upstream, where ENCRYPTED cannot change back to CALL, the call stays muted).

Track the per-call encryption state from the HDU header and LDU2 algorithm ID
(reset at TDU/TDULC). When it is known to be unencrypted, clear the encryption
flag on the LC service options before using them for the channel state and
traffic channel call event. Genuinely encrypted calls are unaffected, and when
the algorithm ID is not yet known (late entry before the first LDU2) the LC
service options are used as before.

Adds VoiceServiceOptions.createUnencrypted(ServiceOptions) to copy service
options with only the encryption flag cleared.

Verified by driving P25P1DecoderState with HDU/LDU1/LDU2/TDU sequences
reproducing the field logs: state events go from C C C E C E C to all CALL;
encrypted calls (ALGID 0x84) still report ENCRYPTED throughout.


Claude-Session: https://claude.ai/code/session_01FRdfTvgcdBvwkipdfP8qxy
(cherry picked from commit 2ef3c9d02176871577d38f8afd0fa3c32fb92a58)

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-13 02:56:00 -05:00
Christopher Smith
1a90eaf6c6 P25P1: decode unconfirmed data PDUs with a packet data SAP as IP packets (#2506)
PDU header format 0x15 is the generic unconfirmed data header; unconfirmed
multi-block trunking control (UMBTC) is only its trunking control SAP
(61/63) use.  PDUHeaderFactory built a plain PDUHeader for every 0x15
header and PDUMessageFactory sent every such sequence to createUMBTC(), so
outbound unconfirmed IP data (SNDCP RF UNCONFIRMED DATA with SAP 0, 4 or
6) was logged raw as

  PDU HEADER FORMAT:UNCONFIRMED MBTC UNCONFIRMED VENDOR:STANDARD TO LLID:n
  DATA BLOCKS:4 MSG:4100450000...

while the confirmed equivalent (format 0x16) was decoded as IPPKT with
its LRRP / ARS payload.

- PDUHeaderFactory: build a PacketHeader for a 0x15 header whose SAP is
  UNENCRYPTED_USER_DATA, PACKET_DATA or SNDCP_PACKET_DATA_CONTROL.  Other
  SAPs, including trunking control 61/63, keep the plain PDUHeader.
- PDUMessageFactory: route a 0x15 sequence whose header is a PacketHeader
  through createPacketData(); everything else stays on the UMBTC path.

No payload changes are needed: PacketMessage already sizes unconfirmed
data blocks at 12 octets, and the header fields it uses (SAP, pad octet
count, data header offset) sit at the same bit positions in the confirmed
and unconfirmed headers.  The ARS RegistrationAcknowledgement parser only
reads the extension octet, so the 5-octet trailer (0x04 + 32-bit time)
some systems append does not affect it.

Adds PDUMessageFactoryTest, built from two unconfirmed PDUs captured on a
Motorola P25 Phase 1 system (ARS registration acknowledgements to UDP
port 4005, one with the trailer), plus header routing checks for SAP
0/4/6, SAP 61/63 and the confirmed 0x16 header.

Co-authored-by: csmith_ai <carrottop@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-13 02:42:53 -05:00
Christopher Smith
fbc54a38dd LRRP: decode request token values as 7-bit varints and correct request token names (#2507)
Motorola LRRP request tokens that carry an integer (trigger interval
0x31, trigger distance 0x78 and the unknown parameters 0x42 / 0x44)
encode it as a 7-bit variable length integer: each byte holds seven
value bits, most significant group first, and the high bit of a byte
flags a continuation byte.  The parser read a single byte, so a real
triggered location start request

  09 11 22 03 FF FF EE 52 44 64 42 9C 10 62 57 34 31 84 58

was logged as

  ID:15663103 TRIGGER ON GPIO TRIGGER EVERY:132 SECS REQUESTED TOKENS
  [TIME,REQUEST-64,REQUEST-62,HORIZONTAL DIRECTION,TIMESTAMP,
  UNRECOGNIZED(10,44,58,9C)]

and now decodes to

  ID:FFFFEE PERIODIC INTERVAL:600 SECS PARAM-42:3600 PARAM-44:100
  REQUESTED TOKENS [TIME,HORIZONTAL SPEED,HORIZONTAL DIRECTION]

The encoding and the token names follow node-dmr-lib (LRRP.js
parseLRRPInt/toLRRPInt and its quoted TRBONet captures, DMRConst.js)
and Moto.Net (LRRPPacket.cs "using VLQ for most of the fields").

- Add VarInt, a shared 7-bit varint reader, and VarIntToken, a token
  base class whose byte length is measured from the value and bounded
  by the remaining payload.  Use them for 0x31, 0x78, 0x42 and 0x44.
- Add TokenType.fromRequestValue(): 0x34 is the periodic trigger flag
  in a request and the timestamp in a response.  TokenFactory accepts
  the resolved TokenType so the request path is not re-resolved.
- Rename request tokens: 0x31 TRIGGER_INTERVAL (TriggerInterval),
  0x4A TRIGGER_CONDITION (TriggerCondition: 02 emergency, 42 pin
  status change, 50 PTT, 51 PTT emergency), 0x78 TRIGGER_DISTANCE
  (TriggerDistance), 0x62 HORIZONTAL_SPEED.  0x42 and 0x44 become
  REQUEST_PARAMETER_42/44 (RequestParameter) with an optional varint
  value; their meaning is unknown (observed 3600 and 100).  Remove
  TriggerGpio and TriggerOnMove.
- Identity: assemble the request id big-endian (it was accumulated
  LSB-first), render it in hex, and clamp the read to the message.
- RequestedTokens: keep a list that holds only unrecognized ids; add
  getters for the recognized and unrecognized lists.
- Remove leftover debug locals from LRRPPacket.parseTokens().
- Add LRRPPacketTest (both captured bodies plus two copies with a
  corrupted type byte) and VarIntTest.

Built with the project's Gradle build (JDK 27 toolchain, in Docker);
compileJava, compileTestJava and the 9 new tests pass.

Co-authored-by: csmith_ai <carrottop@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-13 02:33:52 -05:00
Denny Sheirer
07b61be257 #2632 Detect on startup if multiple versions of any library are on the classpath and warn user to cleanup and reinstall (#2633)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-13 01:59:38 -05:00
Steve Pinkham
74dc32538f P25 Phase 1: fix typo in HDU Golay word starts (278 -> 378) (#2630)
GOLAY_WORD_STARTS listed 278 where it should list 378. The word at 378 was
never corrected, and the decoder ran a correction over bits 278-295 that
straddle the already corrected words at 270 and 288.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 01:31:08 -05:00
Steve Pinkham
7fcf035337 edac: correct Golay(24,12) words that are not at the start of the message (#2631)
Golay24.checkAndCorrect indexed the word from bit 0 (getInt(0, 22), xor(12, ...),
flip(index)) and took the parity over the whole message, so it was only right for a
message that is exactly one 24-bit word. P25 TDULC words 2-12 and the DMR voice
superframe words at 24 and 48 were "corrected" in the wrong bits. Decode any other
word in a 24-bit copy and write it back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 01:29:50 -05:00
Denny Sheirer
a53f6e9d99 #2628 Remove ThumbDV code support. (#2629)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-12 05:18:52 -05:00
Steve Pinkham
e7f4f655e5 C4FM: bounds check the start of the buffer in the sync score (#2622)
A lagging sync detect on the first symbol after a buffer reload, followed by a
coarse timing search that steps all the way left, scores a candidate whose first
sync symbol is one sample before the buffer start. score() only checked the end
of the buffer, so it threw ArrayIndexOutOfBoundsException: Index -1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 03:22:44 -05:00
Denny Sheirer
000ac51ff0 #2621 Windows rtl-sdr crashing on retune documented in issue https://github.com/DSheirer/jRTL/issues/4 (#2624)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-11 03:16:02 -05:00
Denny Sheirer
23cee388de #2616 Gradle Runtime plugin configuration and remove local JDK support in the build script (#2617)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-10 03:42:46 -05:00
Denny Sheirer
5c73fb318f #2614 Gradle not including JFXPanel in build artifacts (#2615)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-10 03:09:41 -05:00
Denny Sheirer
066554393f #2612 Strips unused OS/arch tuner libraries from final build artifacts
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-10 02:46:03 -05:00
Denny Sheirer
303c0a98cc #2610 gradle script cleanup (#2611)
* #2610 Gradle 9.9.0 and build script cleanup

---------

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-10 02:15:51 -05:00
Denny Sheirer
02c62d012f #2557 DMR improved error detection in voice frames and voice events. (#2608)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-09 03:49:58 -05:00
Denny Sheirer
a4ec04a499 Version 1.0.0 beta 1 (#2607)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 10:04:34 -05:00
Denny Sheirer
006e382b34 #2604 jLibUSB library integration (#2605)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 09:56:09 -05:00
tadscottsmith
beb2593dd9 Allow Streaming Configuration Text Inputs to Expand (#2591) 2026-11-08 06:07:56 -05:00
Denny Sheirer
657cbf5c55 #2600 LimeSDR support (#2601)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 06:03:13 -05:00
Denny Sheirer
aff34fa5eb #2566 P25P1 Adjacent Status Broadcast message update. (#2602)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 05:50:30 -05:00
Denny Sheirer
22cd8968cb #1248 Frequency control doesn't allow mouse wheel to change value when locked (#2603)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 05:44:53 -05:00
Denny Sheirer
ec7efa4d17 #536 Add Bias-T support for Airspy, Airspy HF and Funcube Dongles. (#2599)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 04:05:58 -05:00
Denny Sheirer
07fd8c2923 #697 HackRF Bias T control. (#2598)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-07 07:09:54 -05:00
125 changed files with 5765 additions and 5000 deletions

View File

@@ -14,6 +14,9 @@ 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
@@ -34,7 +37,7 @@ jobs:
- name: Create Build Info file - name: Create Build Info file
run: | run: |
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt 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
- 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

View File

@@ -1,6 +1,6 @@
# Publishes a release when a version tag is pushed. The tag has to match # Publishes a release when a version tag is pushed: v0.6.2 for a final release,
# projectVersion in gradle.properties with a leading 'v': v0.6.2 for a final # or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release. The build derives the
# release, or v0.6.2-beta-1 / v0.6.2-alpha-1 for a pre-release. # version from the tag (see build.gradle).
name: Release name: Release
on: on:
@@ -22,15 +22,6 @@ 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
@@ -54,6 +45,17 @@ 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

View File

@@ -54,17 +54,19 @@ 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.
1. Update `projectVersion` in `gradle.properties` to the new version, following the naming convention described in The version is taken from the tag, so there is no version number to update in the code. Create and push a tag
that file (e.g. `0.6.2-alpha-1`, `0.6.2-beta-1` or `0.6.2`). with a leading `v`, following the naming convention described in `gradle.properties` (e.g. `v0.6.2-alpha-1`,
2. Commit and push the change to `master`. `v0.6.2-beta-1` or `v0.6.2`):
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 tag matches `projectVersion` in `gradle.properties` and fails if it does not. * Verifies that the version the build derives from git matches the tag 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

View File

@@ -52,8 +52,44 @@ 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 = project.findProperty('projectVersion') * Version is derived from the most recent 'v<version>' git tag (e.g. v1.0.0-beta-1 -> 1.0.0-beta-1). Commits after
* the tag produce a version like 1.0.0-beta-1-14-g4626d567 and uncommitted changes append '-dirty'. The moving
* 'nightly' tag is ignored. Override with -PprojectVersion=<version> (the nightly build uses 'nightly' so that the
* nightly release asset names stay the same). Falls back to 'unknown' when built outside a git checkout.
*/
String gitDescribe = describeGitVersion()
version = project.findProperty('projectVersion') ?: (gitDescribe ? gitDescribe.substring(1) : 'unknown')
String describeGitVersion()
{
try
{
def describe = providers.exec {
commandLine 'git', 'describe', '--tags', '--match', 'v[0-9]*', '--dirty'
ignoreExitValue = true
}
if(describe.result.get().exitValue == 0)
{
return describe.standardOutput.asText.get().trim()
}
}
catch(Exception e)
{
logger.warn("Unable to determine version from git tags: " + e.message)
}
return null
}
tasks.register('printVersion') {
description = 'Prints the project version'
def projectVersion = version
doLast {
println projectVersion
}
}
//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 {
@@ -95,66 +131,53 @@ test {
dependencies { dependencies {
// JUnit Tests // JUnit Tests
testImplementation(platform("org.junit:junit-bom:6.0.1")) testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1' testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
//Jitpack imports //Jitpack imports
implementation 'com.github.dnault:libresample4j:master' implementation 'com.github.dnault:libresample4j:master'
implementation 'com.github.DSheirer.jmbe:api:v2.1.0' implementation 'com.github.DSheirer.jmbe:api:v2.1.1'
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4' implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports //mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.6.3' implementation 'ch.qos.logback:logback-classic:1.6.6'
implementation 'ch.qos.logback:logback-core:1.5.38' implementation 'ch.qos.logback:logback-core:1.6.6'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.4' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.23.0'
implementation 'com.fazecast:jSerialComm:2.11.4' implementation 'com.formdev:flatlaf:3.8'
implementation 'com.formdev:flatlaf:3.7.2' implementation 'com.formdev:flatlaf-intellij-themes:3.8' //Not imported - Theme.java loads these themes by class name
implementation 'com.formdev:flatlaf-intellij-themes:3.7.2'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1' implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0' implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1' implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.2' implementation('com.github.wendykierp:JTransforms:3.2') {
implementation 'com.google.code.gson:gson:2.14.0' exclude group: 'junit' //JLargeArrays 1.7 declares junit as a compile dependency
implementation 'com.google.guava:guava:33.6.0-jre' }
implementation 'com.google.code.gson:gson:2.15.0'
implementation 'com.google.guava:guava:33.8.0-jre'
implementation 'com.jidesoft:jide-oss:3.6.18' implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:11.4.3' implementation 'com.miglayout:miglayout-swing:11.5.2'
implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.22.0' implementation 'commons-io:commons-io:2.22.0'
implementation 'eu.hansolo:charts:1.0.5' implementation 'io.github.dsheirer:jairspy:1.0.12.1' //Airspy tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jairspy:1.0.12' //Airspy tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer:jairspyhf:1.8.1.1' //Airspy HF+ tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jairspyhf:1.8.1' //Airspy HF+ tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer:jbladerf:2.6.0.1' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer:jhackrf:2026.01.3.1' //HackRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhackrf:2026.01.3' //HackRF tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer:jlibusb:1.0.30.1' //libusb and USB hotplug for the tuner libraries - GitHub release; not pulled in transitively
implementation 'io.github.dsheirer:jhydrasdr:1.1.4' //HydraSDR tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer:jhydrasdr:1.1.4.1' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle implementation 'io.github.dsheirer: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: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:radio-reference-api:18.0.0' implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs implementation 'org.apache.commons:commons-compress:1.28.0' //Not used directly - overrides the vulnerable 1.21 version from jarchivelib
implementation('io.github.dsheirer:usb4java:1.3.5') { //Fork with Loader support for OSX Multi-Version implementation 'org.apache.commons:commons-lang3:3.21.0'
//Excludes the javax.usb API and the duplicate original usb4java classes. The native libraries are listed below.
exclude group: 'org.usb4java', module: 'usb4java-javax'
}
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.28.0'
implementation 'org.apache.commons:commons-lang3:3.20.0'
implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.14.1' implementation 'org.apache.commons:commons-csv:1.14.1'
implementation 'org.apache.mina:mina-core:2.2.9' implementation 'org.apache.mina:mina-core:2.2.10'
implementation 'org.apache.mina:mina-http:2.2.9' implementation 'org.apache.mina:mina-http:2.2.10'
implementation 'org.controlsfx:controlsfx:11.2.3' implementation 'org.controlsfx:controlsfx:11.2.6'
implementation 'org.rauschig:jarchivelib:1.2.0' implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.17' implementation 'org.slf4j:slf4j-api:2.0.20'
implementation 'org.usb4java:libusb4java:1.3.0:darwin-x86-64' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:linux-aarch64' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:linux-arm' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:linux-x86' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:linux-x86-64' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:win32-x86' //usb4java native library
implementation 'org.usb4java:libusb4java:1.3.0:win32-x86-64' //usb4java native library
implementation 'pl.edu.icm:JLargeArrays:1.6'
} }
def os = DefaultNativePlatform.currentOperatingSystem def os = DefaultNativePlatform.currentOperatingSystem
@@ -215,6 +238,7 @@ 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']}",
@@ -245,18 +269,6 @@ String targetOsxX86_64 = 'osx-x86_64-v' + version
String targetWindowsX86_64 = 'windows-x86_64-v' + version String targetWindowsX86_64 = 'windows-x86_64-v' + version
String targetWindowsAarch64 = 'windows-aarch64-v' + version String targetWindowsAarch64 = 'windows-aarch64-v' + version
/**
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
* author's development environment, but can be changed to your environment if you want to use this build pattern.
*/
String jdk_base = '/home/denny/java_jdks/'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
/** /**
* Download URLs to download the JDK as part of the gradle build packaging process * Download URLs to download the JDK as part of the gradle build packaging process
*/ */
@@ -285,19 +297,138 @@ def configure(RuntimeZipTask rt, List<String> jvmArgs) {
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis //jdk.incubator.vector - needed for Project Panama foreign function and vector apis
//jdk.accessibility is used with assistive technologies like screen readers //jdk.accessibility is used with assistive technologies like screen readers
//java.management for JVM resource monitoring //java.management for JVM resource monitoring
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management') //java.base, java.logging, java.net.http, java.prefs, javafx.controls, javafx.swing - required by sdrtrunk (jdeps)
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management',
'java.base', 'java.logging', 'java.net.http', 'java.prefs', 'javafx.controls', 'javafx.swing')
//Use auto-detected modules and 'add' any specified modules. //Use only the modules listed above. The runtime plugin's module auto-detection uses a bundled ASM library that
rt.extension.additive.set(true) //can't read Java 27 class files and logs a 'Failed to scan' error for each sdrtrunk class. The jdeps module check
//on the runtime task verifies that this list is complete.
rt.extension.additive.set(false)
//Recommended options for reducing/shrinking runtime target image size //Recommended options for reducing/shrinking runtime target image size
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages') rt.extension.addOptions('--strip-debug', '--compress', 'zip-6', '--no-header-files', '--no-man-pages')
//Runtime images have auto appended version in image zip name. Override to include version in single target image. //Runtime images have auto appended version in image zip name. Override to include version in single target image.
// rt.extension.imageDir=layout.buildDirectory.dir('image') // rt.extension.imageDir=layout.buildDirectory.dir('image')
rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip'))) rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip')))
} }
/**
* Rewrites the jar file without the native library folders for other operating systems and architectures. Fails the
* build if a jar that contains native libraries doesn't contain the folder for the target.
* @param jar file to rewrite
* @param keep native folder for the target
* @return number of bytes removed from the jar
*/
long stripJar(File jar, String keep) {
def tunerPattern = ~/^[^\/]+\/native\/([^\/]+)\/.*/
def removed = [] as Set
def hadTuner = false, keptTuner = false
File temp = new File(jar.parentFile, jar.name + '.tmp')
new java.util.zip.ZipFile(jar).withCloseable { zip ->
def entries = zip.entries().toList()
entries.each { e ->
def tuner = tunerPattern.matcher(e.name)
if(tuner.matches()) {
hadTuner = true
if(tuner.group(1) == keep) { keptTuner = true } else { removed << e.name }
}
}
if(hadTuner && !keptTuner) {
throw new GradleException("Native strip: ${jar.name} has no native/${keep} folder")
}
if(!removed.isEmpty()) {
new java.util.zip.ZipOutputStream(new FileOutputStream(temp)).withCloseable { out ->
entries.findAll { !removed.contains(it.name) }.each { e ->
def copy = new java.util.zip.ZipEntry(e)
if(copy.method == java.util.zip.ZipEntry.DEFLATED) {
copy.compressedSize = -1
}
out.putNextEntry(copy)
zip.getInputStream(e).withCloseable { out << it }
out.closeEntry()
}
}
}
}
if(removed.isEmpty()) {
return 0
}
long before = jar.length()
java.nio.file.Files.move(temp.toPath(), jar.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING)
return before - jar.length()
}
/**
* Removes the native libraries for other operating systems and architectures from the jars in each runtime target
* image, before the runtimeZip tasks package the images. The tuner libraries (jRTL, jAirspy, etc.) package their
* native libraries under '<library>/native/<os>-<arch>/', where the folder name matches the runtime target name
* '<os>-<arch>-v<version>' without the version suffix, except that the libraries use 'macos' where the target uses 'osx'.
*/
tasks.named('runtime') {rt ->
rt.doLast {
rt.targetPlatforms.get().keySet().each { target ->
def nativeFolder = (target - "-v${version}").replaceFirst(/^osx-/, 'macos-')
def lib = new File(rt.imageDir.asFile, "${project.name}-${target}/lib")
long total = 0
lib.listFiles({ File f -> f.name.endsWith('.jar') } as FileFilter).each { jar ->
total += stripJar(jar, nativeFolder)
}
logger.lifecycle("Native strip: removed ${String.format('%.1f', total / 1048576)} MB of other " +
"platform native libraries from [${target}]")
}
}
}
/**
* Verifies that each runtime target image contains every module that the sdrtrunk jar requires, as reported by jdeps.
* This guards against the runtime plugin's module auto-detection silently missing modules (e.g. when it can't parse
* class files compiled for a newer Java version).
*/
def jdepsHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
def mainJar = tasks.named('jar').flatMap { it.archiveFile }
tasks.named('runtime') {rt ->
rt.doLast {
def jdeps = new File(jdepsHome.get(), 'bin/' + (os.isWindows() ? 'jdeps.exe' : 'jdeps')).absolutePath
def process = new ProcessBuilder(jdeps, '--multi-release', java.toolchain.languageVersion.get().toString(),
'--ignore-missing-deps', '--print-module-deps', '-q', mainJar.get().asFile.absolutePath)
.redirectErrorStream(true).start()
def output = process.inputStream.text.trim()
if(process.waitFor() != 0) {
throw new GradleException("Module check: jdeps failed: ${output}")
}
def required = output.split(',')*.trim().findAll { it } as Set
rt.targetPlatforms.get().keySet().each { target ->
def release = new File(rt.imageDir.asFile, "${project.name}-${target}/release")
def match = release.text =~ /(?m)^MODULES="([^"]*)"/
if(!match.find()) {
throw new GradleException("Module check: no MODULES entry in ${release}")
}
def missing = required - (match.group(1).split(' ') as Set)
if(missing) {
throw new GradleException("Module check: runtime image [${target}] is missing required modules " +
"${missing.sort()} - add them to addModules() in build.gradle")
}
}
logger.lifecycle("Module check: runtime images contain all modules required by sdrtrunk: ${required.sort()}")
}
}
/** /**
* Creates a release package for the current operating system. * Creates a release package for the current operating system.
* *
@@ -354,22 +485,12 @@ tasks.register('runtimeZipCurrent', RuntimeZipTask) {rt ->
*/ */
tasks.register('runtimeZipWindows', RuntimeZipTask) {rt -> tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
//Windows aarch64 //Windows aarch64
if(file(jdk_windows_aarch64).exists()) { rt.extension.targetPlatform(targetWindowsAarch64) {
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, []) jdkHome = jdkDownload(jdk_download_windows_aarch64)
}
else {
rt.extension.targetPlatform(targetWindowsAarch64) {
jdkHome = jdkDownload(jdk_download_windows_aarch64)
}
} }
//Windows x86-64 //Windows x86-64
if(file(jdk_windows_x86_64).exists()) { rt.extension.targetPlatform(targetWindowsX86_64) {
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, []) jdkHome = jdkDownload(jdk_download_windows_x86_64)
}
else {
rt.extension.targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload(jdk_download_windows_x86_64)
}
} }
configure(rt, jvmArgsWindows) configure(rt, jvmArgsWindows)
@@ -386,40 +507,20 @@ tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
*/ */
tasks.register('runtimeZipOthers', RuntimeZipTask) {rt -> tasks.register('runtimeZipOthers', RuntimeZipTask) {rt ->
//Linux aarch64 //Linux aarch64
if(file(jdk_linux_aarch64).exists()) { rt.extension.targetPlatform(targetLinuxAarch64) {
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, []) jdkHome = jdkDownload(jdk_download_linux_aarch64)
}
else {
rt.extension.targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload(jdk_download_linux_aarch64)
}
} }
//Linux x86-64 //Linux x86-64
if(file(jdk_linux_x86_64).exists()) { rt.extension.targetPlatform(targetLinuxX86_64) {
rt.extension.targetPlatform(targetLinuxX86_64, jdk_linux_x86_64, []) jdkHome = jdkDownload(jdk_download_linux_x86_64)
}
else {
rt.extension.targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload(jdk_download_linux_x86_64)
}
} }
//OSX aarch64 //OSX aarch64
if(file(jdk_osx_aarch64).exists()) { rt.extension.targetPlatform(targetOsxAarch64) {
rt.extension.targetPlatform(targetOsxAarch64, jdk_osx_aarch64, []) jdkHome = jdkDownload(jdk_download_osx_aarch64)
}
else {
rt.extension.targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload(jdk_download_osx_aarch64)
}
} }
//OSX x86-64 //OSX x86-64
if(file(jdk_osx_x86_64).exists()) { rt.extension.targetPlatform(targetOsxX86_64) {
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, []) jdkHome = jdkDownload(jdk_download_osx_x86_64)
}
else {
rt.extension.targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload(jdk_download_osx_x86_64)
}
} }
configure(rt, jvmArgsLinux) configure(rt, jvmArgsLinux)

View File

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

Binary file not shown.

View File

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

52
gradlew.bat vendored
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' } maven { url = 'https://jitpack.io' }
//jAirspy is published as a GitHub release, with the native libraries for all platforms in the jar //jAirspy is published as a GitHub release, with the native libraries for all platforms in the jar
ivy { ivy {
@@ -33,6 +33,15 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jrtl' } content { includeModule 'io.github.dsheirer', 'jrtl' }
} }
//jlibusb is published as a GitHub release, with libusb for all platforms in the jar. Tuner libraries built on
//jlibusb (jAirspy, jAirspyHF, jBladeRF, jHackRF, jHydraSDR, jLimeSDR, jRTL) load their native library next to its libusb, so the process shares one copy.
ivy {
url = 'https://github.com/DSheirer/jlibusb/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jlibusb' }
}
//jHackRF is published as a GitHub release, with the native libraries for all platforms in the jar //jHackRF is published as a GitHub release, with the native libraries for all platforms in the jar
ivy { ivy {
url = 'https://github.com/DSheirer/jHackRF/releases/download' url = 'https://github.com/DSheirer/jHackRF/releases/download'
@@ -49,6 +58,14 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jhydrasdr' } content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
} }
//jLimeSDR is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jLimeSDR/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jlimesdr' }
}
//jsdrplay is published as a GitHub release. The jar does not contain the SDRplay API native library, which //jsdrplay is published as a GitHub release. The jar does not contain the SDRplay API native library, which
//users install separately from sdrplay.com //users install separately from sdrplay.com
ivy { ivy {

View File

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

View File

@@ -1,44 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message;
/**
* AMBE-3000R Message
*/
public abstract class AmbeMessage
{
/**
* Converts the byte array to a hexadecimal string array
*/
public static String toHex(byte[] data)
{
StringBuilder sb = new StringBuilder();
for(byte b: data)
{
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}

View File

@@ -1,95 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message;
import io.github.dsheirer.audio.convert.thumbdv.message.response.DecodeSpeechResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.EncodeSpeechResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.GetConfigResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.InitializeCodecResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.ProductIdResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetChannelFormatResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetChannelResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetPacketModeResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetSpeechFormatResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.UnknownResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.VersionResponse;
public class AmbeMessageFactory
{
private static final byte CONTROL_PACKET = (byte)0x00;
private static final byte CHANNEL_PACKET = (byte)0x01;
private static final byte SPEECH_PACKET = (byte)0x02;
private static final int INDEX_PACKET_TYPE = 3;
private static final int INDEX_CONTROL_PACKET_TYPE = 4;
public static AmbeMessage getMessage(byte[] data)
{
if(data != null && data.length >= 5)
{
if(data[INDEX_PACKET_TYPE] == CONTROL_PACKET)
{
PacketField packetField = PacketField.fromValue(data[INDEX_CONTROL_PACKET_TYPE]);
switch(packetField)
{
case PKT_CHANNEL_0:
return new SetChannelResponse(data);
case PKT_CHANNEL_FORMAT:
return new SetChannelFormatResponse(data);
case PKT_CODEC_STOP:
return new SetPacketModeResponse(data);
case PKT_GET_CONFIG:
return new GetConfigResponse(data);
case PKT_INIT:
return new InitializeCodecResponse(data);
case PKT_PRODUCT_ID:
return new ProductIdResponse(data);
case PKT_RATE_PARAMETER:
return new SetVocoderParameterResponse(data);
case PKT_RATE_TABLE:
return new SetVocoderResponse(data);
case PKT_READY:
return new ReadyResponse(data);
case PKT_SPEECH_FORMAT:
return new SetSpeechFormatResponse(data);
case PKT_VERSION_STRING:
return new VersionResponse(data);
}
}
else if(data[INDEX_PACKET_TYPE] == CHANNEL_PACKET)
{
return new EncodeSpeechResponse(data);
}
else if(data[INDEX_PACKET_TYPE] == SPEECH_PACKET)
{
return new DecodeSpeechResponse(data);
}
}
return new UnknownResponse(data);
}
}

View File

@@ -1,47 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message;
/**
* AMBE-3000 Initialization Options
*/
public enum InitializeOption
{
ENCODER((byte)0x01),
DECODER((byte)0x02),
ENCODER_AND_DECODER((byte)0x03),
ECHO_CANCELLER((byte)0x04),
ENCODER_DECODER_ECHO_CANCELLER((byte)0x07);
private byte mCode;
InitializeOption(byte code)
{
mCode = code;
}
public byte getCode()
{
return mCode;
}
}

View File

@@ -1,160 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.convert.thumbdv.message;
/**
* AMBE-3000R Control Packet Field enumeration
*/
public enum PacketField
{
PKT_ENCODER_CMODE((byte)0x05),
PKT_DECODER_CMODE((byte)0x06),
PKT_RATE_TABLE((byte)0x09),
PKT_RATE_PARAMETER((byte)0x0A),
PKT_INIT((byte)0x0B),
PKT_LOW_POWER((byte)0x10),
PKT_CHANNEL_FORMAT((byte)0x15),
PKT_SPEECH_FORMAT((byte)0x16),
PKT_CODEC_START((byte)0x2A),
PKT_CODEC_STOP((byte)0x2B),
PKT_PRODUCT_ID((byte)0x30),
PKT_VERSION_STRING((byte)0x31),
PKT_COMPAND((byte)0x32),
PKT_RESET((byte)0x33),
PKT_RESET_TO_SOFTWARE_CONFIG((byte)0x34),
PKT_HALT((byte)0x35),
PKT_GET_CONFIG((byte)0x36),
PKT_READ_CONFIG((byte)0x37),
PKT_CODEC_CFG((byte)0x38),
PKT_READY((byte)0x39),
PKT_PARITY_MODE((byte)0x3F),
PKT_CHANNEL_0((byte)0x40),
PKT_WRITE_I2C((byte)0x44),
PKT_CLEAR_CODEC_RESET((byte)0x46),
PKT_SET_CODEC_RESET((byte)0x47),
PKT_DISCARD_CODEC_SAMPLES((byte)0x48),
PKT_DELAY_NUMBER_MICRO_SECONDS((byte)0x49),
PKT_DELAY_NUMBER_NANO_SECONDS((byte)0x4A),
PKT_GAIN((byte)0x4B),
PKT_RTS_THRESHOLD((byte)0x4E),
//Speech packet values
SPEECH_DATA((byte)0x00),
CHANNEL_DATA_HARD_SYMBOL((byte)0x01),
CMODE((byte)0x02),
SAMPLE_COUNT((byte)0x03),
TONE((byte)0x08),
CHANNEL_DATA_SOFT_SYMBOL((byte)0x17),
VOCODER((byte)0x40),
//Packet Types
PACKET_TYPE_CONTROL((byte)0x00),
PACKET_TYPE_ENCODE_SPEECH((byte)0x01),
PACKET_TYPE_DECODE_SPEECH((byte)0x02),
UNKNOWN((byte)0x00);
private byte mCode;
PacketField(byte code)
{
mCode = code;
}
/**
* Packet type byte code value
*/
public byte getCode()
{
return mCode;
}
/**
* Lookup a packet type from the code byte value
*/
public static PacketField fromValue(byte value)
{
switch(value)
{
case (byte)0x05:
return PKT_ENCODER_CMODE;
case (byte)0x06:
return PKT_DECODER_CMODE;
case (byte)0x09:
return PKT_RATE_TABLE;
case (byte)0x0A:
return PKT_RATE_PARAMETER;
case (byte)0x0B:
return PKT_INIT;
case (byte)0x10:
return PKT_LOW_POWER;
case (byte)0x15:
return PKT_CHANNEL_FORMAT;
case (byte)0x16:
return PKT_SPEECH_FORMAT;
case (byte)0x2A:
return PKT_CODEC_START;
case (byte)0x2B:
return PKT_CODEC_STOP;
case (byte)0x30:
return PKT_PRODUCT_ID;
case (byte)0x31:
return PKT_VERSION_STRING;
case (byte)0x32:
return PKT_COMPAND;
case (byte)0x33:
return PKT_RESET;
case (byte)0x34:
return PKT_RESET_TO_SOFTWARE_CONFIG;
case (byte)0x35:
return PKT_HALT;
case (byte)0x36:
return PKT_GET_CONFIG;
case (byte)0x37:
return PKT_READ_CONFIG;
case (byte)0x38:
return PKT_CODEC_CFG;
case (byte)0x39:
return PKT_READY;
case (byte)0x3F:
return PKT_PARITY_MODE;
case (byte)0x40:
return PKT_CHANNEL_0;
case (byte)0x44:
return PKT_WRITE_I2C;
case (byte)0x46:
return PKT_CLEAR_CODEC_RESET;
case (byte)0x47:
return PKT_SET_CODEC_RESET;
case (byte)0x48:
return PKT_DISCARD_CODEC_SAMPLES;
case (byte)0x49:
return PKT_DELAY_NUMBER_MICRO_SECONDS;
case (byte)0x4A:
return PKT_DELAY_NUMBER_NANO_SECONDS;
case (byte)0x4B:
return PKT_GAIN;
case (byte)0x4E:
return PKT_RTS_THRESHOLD;
default:
return UNKNOWN;
}
}
}

View File

@@ -1,129 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message;
public enum VocoderRate
{
//AMBE-1000 Standard Rates
RATE_0((byte)0x00, 2400,0),
RATE_1((byte)0x01, 3600,0),
RATE_2((byte)0x02, 3600,1200),
RATE_3((byte)0x03, 4800,0),
RATE_4((byte)0x04, 9600,0),
RATE_5((byte)0x05, 2350,50),
RATE_6((byte)0x06, 4850,4750),
RATE_7((byte)0x07, 4550,250),
RATE_8((byte)0x08, 3100,1700),
RATE_9((byte)0x09, 4400,2800),
RATE_10((byte)0x0A, 4150,2250),
RATE_11((byte)0x0B, 3350,250),
RATE_12((byte)0x0C, 7750,250),
RATE_13((byte)0x0D, 4650,3350),
RATE_14((byte)0x0E, 3750,250),
RATE_15((byte)0x0F, 4000,0),
//AMBE-2000 Standard Rates
RATE_16((byte)0x10, 3600,0),
RATE_17((byte)0x11, 4000,0),
RATE_18((byte)0x12, 4800,0),
RATE_19((byte)0x13, 6400,0),
RATE_20((byte)0x14, 8000,0),
RATE_21((byte)0x15, 9600,0),
RATE_22((byte)0x16, 2400,1600),
RATE_23((byte)0x17, 3600,1200),
RATE_24((byte)0x18, 4000,800),
RATE_25((byte)0x19, 2400,2400),
RATE_26((byte)0x1A, 4000,2400),
RATE_27((byte)0x1B, 4400,2800), //Is this compatible with APCO25 Phase 1?
RATE_28((byte)0x1C, 4000,4000),
RATE_29((byte)0x1D, 2400,7200),
RATE_30((byte)0x1E, 3600,6000),
RATE_31((byte)0x1F, 2000,0),
RATE_32((byte)0x20, 3600,2800),
//AMBE-3000 Standard Rates (AMBE+2)
RATE_33((byte)0x21, 2450,1150), //APCO25 HR (Phase 2), DMR, NXDN, DSTAR
RATE_34((byte)0x22, 2450,0),
RATE_35((byte)0x23, 2250,1150),
RATE_36((byte)0x24, 2250,0),
RATE_37((byte)0x25, 2400,0),
RATE_38((byte)0x26, 3000,0),
RATE_39((byte)0x27, 3600,0),
RATE_40((byte)0x28, 4000,0),
RATE_41((byte)0x29, 4400,0),
RATE_42((byte)0x2A, 4800,0),
RATE_43((byte)0x2B, 6400,0),
RATE_44((byte)0x2C, 7200,0),
RATE_45((byte)0x2D, 8000,0),
RATE_46((byte)0x2E, 9600,0),
RATE_47((byte)0x2F, 2450,250),
RATE_48((byte)0x30, 3350,250),
RATE_49((byte)0x31, 3750,250),
RATE_50((byte)0x32, 4550,250),
RATE_51((byte)0x33, 2450,1950),
RATE_52((byte)0x34, 2450,2350),
RATE_53((byte)0x35, 2450,3550),
RATE_54((byte)0x36, 2450,4750),
RATE_55((byte)0x37, 2600,1400),
RATE_56((byte)0x38, 3600,1200),
RATE_57((byte)0x39, 4000,800),
RATE_58((byte)0x3A, 4000,2400),
RATE_59((byte)0x3B, 4400,2800), //Is this compatible with APCO25 Phase 1?
RATE_60((byte)0x3C, 4000,4000),
RATE_61((byte)0x3D, 3600,6000);
private byte mCode;
private int mSpeechRate;
private int mFecRate;
VocoderRate(byte code, int speechRate, int fecRate)
{
mCode = code;
mSpeechRate = speechRate;
mFecRate = fecRate;
}
/**
* Byte code value for the vocoder rate
*/
public byte getCode()
{
return mCode;
}
/**
* Speech rate in bits per second (bps)
*/
public int getSpeechRate()
{
return mSpeechRate;
}
/**
* Forward Error Correction rate in bits per second (bps)
*/
public int getFecRate()
{
return mFecRate;
}
}

View File

@@ -1,67 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessage;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* AMBE-3000R Request Packet
*/
public abstract class AmbeRequest extends AmbeMessage
{
private static final byte PACKET_START_BYTE = (byte)0x61;
protected static final int PACKET_TYPE_INDEX = 3;
protected static final int PAYLOAD_START_INDEX = 4;
public abstract PacketField getType();
public abstract byte[] getData();
/**
* Creates a byte array of the specified length plus 4 packet header bytes with packet start, length and control
* bytes filled in.
*
* @param length of the message (not including the 4 byte packet header)
* @return byte array with packet header pre-filled.
*/
protected byte[] createMessage(int length, PacketField type)
{
byte[] data = new byte[length + 4];
data[0] = PACKET_START_BYTE;
data[1] = (byte)((length >> 8 & 0xFF));
data[2] = (byte)(length & 0xFF);
if(type == PacketField.PACKET_TYPE_ENCODE_SPEECH || type == PacketField.PACKET_TYPE_DECODE_SPEECH)
{
data[PACKET_TYPE_INDEX] = type.getCode();
}
else
{
data[PACKET_TYPE_INDEX] = PacketField.PACKET_TYPE_CONTROL.getCode();
data[PAYLOAD_START_INDEX] = type.getCode();
}
return data;
}
}

View File

@@ -1,108 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.audio.convert.thumbdv.message.VocoderRate;
/**
* Decode speech request, used to request decode of an encoded audio frame.
*/
public class DecodeSpeechRequest extends AmbeRequest
{
private static final int CHANNEL_DATA_IDENTIFIER_INDEX = 4;
private static final byte SAMPLE_COUNT = (byte)(0xFF & 160); //8 kHz Audio sample count for 20ms frame
private byte[] mAudioFrame;
private VocoderRate mVocoderRate;
/**
* Constructs an audio frame decode request using the specified vocoder rate.
* @param audioFrame of encoded audio samples
* @param vocoderRate to use when decoding
*/
public DecodeSpeechRequest(byte[] audioFrame, VocoderRate vocoderRate)
{
mAudioFrame = audioFrame;
mVocoderRate = vocoderRate;
}
/**
* Constructs an audio frame decode request using the current vocoder rate.
* @param audioFrame of encoded audio samples
*/
public DecodeSpeechRequest(byte[] audioFrame)
{
this(audioFrame, null);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_ENCODE_SPEECH;
}
private boolean hasVocoderRate()
{
return mVocoderRate != null;
}
@Override
public byte[] getData()
{
if(hasVocoderRate())
{
int length = mAudioFrame.length + 9;
byte[] data = createMessage(length, getType());
int offset = CHANNEL_DATA_IDENTIFIER_INDEX;
data[offset++] = PacketField.VOCODER.getCode();
data[offset++] = mVocoderRate.getCode();
data[offset++] = PacketField.CHANNEL_DATA_HARD_SYMBOL.getCode();
data[offset++] = (byte)(0xFF & (mAudioFrame.length * 8));
System.arraycopy(mAudioFrame, 0, data, offset, mAudioFrame.length);
offset += mAudioFrame.length;
data[offset++] = PacketField.SAMPLE_COUNT.getCode();
data[offset++] = (byte)0xA0;
data[offset++] = (byte)0x02;
data[offset++] = (byte)0x00;
data[offset] = (byte)0x00;
return data;
}
else
{
int length = mAudioFrame.length + 2;
byte[] data = createMessage(length, getType());
int offset = CHANNEL_DATA_IDENTIFIER_INDEX;
data[offset++] = PacketField.CHANNEL_DATA_HARD_SYMBOL.getCode();
data[offset++] = (byte)(0xFF & (mAudioFrame.length * 8));
System.arraycopy(mAudioFrame, 0, data, offset, mAudioFrame.length);
return data;
}
}
}

View File

@@ -1,88 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
public class EncodeSpeechRequest extends AmbeRequest
{
private static final int CHANNEL_IDENTIFIER_INDEX = 4;
private static final int SPEECH_DATA_IDENTIFIER_INDEX = 5;
private static final int SAMPLE_COUNT_INDEX = 6;
private static final int SPEECH_DATA_START_INDEX = 7;
private short[] mSamples;
public EncodeSpeechRequest(short[] samples)
{
mSamples = samples;
}
public EncodeSpeechRequest(float[] samples)
{
mSamples = new short[samples.length];
for(int x = 0; x < samples.length; x++)
{
if(samples[x] > 1.0f)
{
mSamples[x] = Short.MAX_VALUE;
}
else if(samples[x] < -1.0f)
{
mSamples[x] = Short.MIN_VALUE;
}
else
{
mSamples[x] = (short)(samples[x] * Short.MAX_VALUE);
}
}
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_DECODE_SPEECH;
}
@Override
public byte[] getData()
{
int length = (mSamples.length * 2) + 3;
byte[] data = createMessage(length, getType());
data[CHANNEL_IDENTIFIER_INDEX] = PacketField.PKT_CHANNEL_0.getCode();
data[SPEECH_DATA_IDENTIFIER_INDEX] = (byte)0x00;
data[SAMPLE_COUNT_INDEX] = (byte)(0xFF & mSamples.length);
int pointer = SPEECH_DATA_START_INDEX;
for(short sample: mSamples)
{
data[pointer++] = (byte)((sample >> 8 & 0xFF));
data[pointer++] = (byte)(sample & 0xFF);
}
return data;
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Get configuration request packet
*/
public class GetConfigRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_GET_CONFIG;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,54 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.InitializeOption;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Initialize CODEC request packet
*/
public class InitializeCodecRequest extends AmbeRequest
{
private static final int INITIALIZE_OPTION_INDEX = 5;
private InitializeOption mInitializeOption;
public InitializeCodecRequest(InitializeOption option)
{
mInitializeOption = option;
}
@Override
public PacketField getType()
{
return PacketField.PKT_INIT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[INITIALIZE_OPTION_INDEX] = mInitializeOption.getCode();
return data;
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Product ID request packet
*/
public class ProductIdRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_PRODUCT_ID;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Reset request packet
*/
public class ResetRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_RESET;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,48 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel format request packet
*/
public class SetChannelFormatRequest extends AmbeRequest
{
private static final int FORMAT_INDEX = 5;
private static final byte FORMAT = (byte)0x00;
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_FORMAT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[FORMAT_INDEX] = FORMAT;
return data;
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel request packet
*/
public class SetChannelRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_0;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Packet mode (versus CODEC mode) request packet
*/
public class SetPacketModeRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_CODEC_STOP;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,48 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set speech format request packet
*/
public class SetSpeechFormatRequest extends AmbeRequest
{
private static final int FORMAT_INDEX = 5;
private static final byte FORMAT = (byte)0x00;
@Override
public PacketField getType()
{
return PacketField.PKT_SPEECH_FORMAT;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[FORMAT_INDEX] = FORMAT;
return data;
}
}

View File

@@ -1,74 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder parameters request packet
*/
public class SetVocoderParametersRequest extends AmbeRequest
{
private int mWord0;
private int mWord1;
private int mWord2;
private int mWord3;
private int mWord4;
private int mWord5;
public SetVocoderParametersRequest(int word0, int word1, int word2, int word3, int word4, int word5)
{
mWord0 = word0;
mWord1 = word1;
mWord2 = word2;
mWord3 = word3;
mWord4 = word4;
mWord5 = word5;
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_PARAMETER;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(14, getType());
data[PAYLOAD_START_INDEX + 1] = (byte)(mWord0 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 2] = (byte)(mWord0 & 0xFF);
data[PAYLOAD_START_INDEX + 3] = (byte)(mWord1 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 4] = (byte)(mWord1 & 0xFF);
data[PAYLOAD_START_INDEX + 5] = (byte)(mWord2 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 6] = (byte)(mWord2 & 0xFF);
data[PAYLOAD_START_INDEX + 7] = (byte)(mWord3 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 8] = (byte)(mWord3 & 0xFF);
data[PAYLOAD_START_INDEX + 9] = (byte)(mWord4 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 10] = (byte)(mWord4 & 0xFF);
data[PAYLOAD_START_INDEX + 11] = (byte)(mWord5 >> 8 & 0xFF);
data[PAYLOAD_START_INDEX + 12] = (byte)(mWord5 & 0xFF);
return data;
}
}

View File

@@ -1,53 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.audio.convert.thumbdv.message.VocoderRate;
/**
* Set Vocoder Rate request packet
*/
public class SetVocoderRequest extends AmbeRequest
{
private VocoderRate mVocoderRate;
public SetVocoderRequest(VocoderRate rate)
{
mVocoderRate = rate;
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_TABLE;
}
@Override
public byte[] getData()
{
byte[] data = createMessage(2, getType());
data[PAYLOAD_START_INDEX + 1] = mVocoderRate.getCode();
return data;
}
}

View File

@@ -1,43 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.request;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Version string request packet
*/
public class VersionRequest extends AmbeRequest
{
@Override
public PacketField getType()
{
return PacketField.PKT_VERSION_STRING;
}
@Override
public byte[] getData()
{
return createMessage(1, getType());
}
}

View File

@@ -1,60 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.AmbeMessage;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* AMBE-3000R Response Packet
*/
public abstract class AmbeResponse extends AmbeMessage
{
protected static final int PAYLOAD_START_INDEX = 5;
private byte[] mMessage;
protected AmbeResponse(byte[] message)
{
mMessage = message;
}
/**
* Control packet type
*/
public abstract PacketField getType();
/**
* Received message bytes
*/
protected byte[] getMessage()
{
return mMessage;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX, getMessage().length);
}
}

View File

@@ -1,59 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2019 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import io.github.dsheirer.sample.ConversionUtils;
import java.util.Arrays;
/**
* Decode speech response
*/
public class DecodeSpeechResponse extends AmbeResponse
{
public DecodeSpeechResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_DECODE_SPEECH;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX + 1, getMessage().length);
}
public float[] getSamples()
{
return ConversionUtils.convertFromSigned16BitSamples(getPayload());
}
@Override
public String toString()
{
return "DECODED SPEECH: SAMPLE COUNT:" + getSamples().length + " " + toHex(getPayload());
}
}

View File

@@ -1,59 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Encode speech response
*/
public class EncodeSpeechResponse extends AmbeResponse
{
public EncodeSpeechResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PACKET_TYPE_ENCODE_SPEECH;
}
/**
* Payload of the packet (does not include the packet header)
*/
protected byte[] getPayload()
{
return Arrays.copyOfRange(getMessage(), PAYLOAD_START_INDEX + 1, getMessage().length);
}
@Override
public String toString()
{
return "ENCODED SPEECH: " + toHex(getPayload()) + " " + Arrays.toString(getMessage());
}
}

View File

@@ -1,49 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Get configuration response
*/
public class GetConfigResponse extends AmbeResponse
{
public GetConfigResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_GET_CONFIG;
}
@Override
public String toString()
{
return "CONFIGURATION: " + toHex(getPayload());
}
}

View File

@@ -1,58 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Initialize CODEC response
*/
public class InitializeCodecResponse extends AmbeResponse
{
public InitializeCodecResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_INIT;
}
/**
* Success / fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "INITIALIZE CODEC " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,56 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Product ID Response
*/
public class ProductIdResponse extends AmbeResponse
{
public ProductIdResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_PRODUCT_ID;
}
/**
* Product ID string
*/
public String getProductId()
{
return new String(getPayload()).trim();
}
@Override
public String toString()
{
return "PRODUCT ID:" + getProductId();
}
}

View File

@@ -1,50 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Ready response
*/
public class ReadyResponse extends AmbeResponse
{
public ReadyResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_READY;
}
@Override
public String toString()
{
return "READY" + Arrays.toString(getMessage());
}
}

View File

@@ -1,57 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Reset response
*/
public class ResetResponse extends AmbeResponse
{
public ResetResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RESET;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "RESET " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,58 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set channel format response
*/
public class SetChannelFormatResponse extends AmbeResponse
{
public SetChannelFormatResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_FORMAT;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET CHANNEL FORMAT " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,57 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder rate response
*/
public class SetChannelResponse extends AmbeResponse
{
public SetChannelResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CHANNEL_0;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET CHANNEL 0 " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,58 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set packet mode response
*/
public class SetPacketModeResponse extends AmbeResponse
{
public SetPacketModeResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_CODEC_STOP;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET PACKET MODE " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,58 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set speech format response
*/
public class SetSpeechFormatResponse extends AmbeResponse
{
public SetSpeechFormatResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_SPEECH_FORMAT;
}
/**
* Success or fail
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET SPEECH FORMAT " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,65 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder parameter response
*/
public class SetVocoderParameterResponse extends AmbeResponse
{
public SetVocoderParameterResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_PARAMETER;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==3 && payload[0] == 0;
}
@Override
public String toString()
{
if(isSuccessful())
{
return "SET VOCODER RATE SUCCESSFUL";
}
else
{
return "SET VOCODER RATE **FAILED** - RESPONSE:" + toHex(getPayload());
}
}
}

View File

@@ -1,58 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Set Vocoder rate response
*/
public class SetVocoderResponse extends AmbeResponse
{
public SetVocoderResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_RATE_TABLE;
}
/**
* Success indicator
*/
public boolean isSuccessful()
{
byte[] payload = getPayload();
return payload != null && payload.length ==1 && payload[0] == 0;
}
@Override
public String toString()
{
return "SET VOCODER RATE " + (isSuccessful() ? "SUCCESSFUL" : "**FAILED**");
}
}

View File

@@ -1,50 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
import java.util.Arrays;
/**
* Unknown Response Message
*/
public class UnknownResponse extends AmbeResponse
{
public UnknownResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.UNKNOWN;
}
@Override
public String toString()
{
return "UNKNOWN MESSAGE:" + Arrays.toString(getMessage());
}
}

View File

@@ -1,56 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.convert.thumbdv.message.response;
import io.github.dsheirer.audio.convert.thumbdv.message.PacketField;
/**
* Version Response
*/
public class VersionResponse extends AmbeResponse
{
public VersionResponse(byte[] message)
{
super(message);
}
@Override
public PacketField getType()
{
return PacketField.PKT_VERSION_STRING;
}
/**
* Version string
*/
public String getVersion()
{
return new String(getPayload()).trim();
}
@Override
public String toString()
{
return "VERSION:" + getVersion();
}
}

View File

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

View File

@@ -68,6 +68,29 @@ 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);

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer * Copyright (C) 2014-2026 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,6 +60,7 @@ 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;
@@ -81,6 +82,7 @@ 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;
@@ -153,6 +155,17 @@ 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();
@@ -253,6 +266,11 @@ 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())
@@ -909,6 +927,28 @@ 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.
*/ */

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer * Copyright (C) 2014-2026 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,6 +133,7 @@ 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"));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,25 +123,23 @@ 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.fromValue(tokenId); TokenType tokenType = TokenType.fromRequestValue(tokenId);
if(tokenType.isRequestParameterToken()) if(tokenType.isRequestParameterToken())
{ {
Token token = TokenFactory.createToken(tokenId, getMessage(), offset, characterCount); Token token = TokenFactory.createToken(tokenType, 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);

View File

@@ -26,7 +26,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
* LRRP Identity Token * LRRP Identity Token
* *
* Start Token: 0x22 * Start Token: 0x22
* Total Length: 6 bytes * Total Length: 2 bytes plus a 1-4 byte request identifier
*/ */
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 a heading token. * Constructs an instance of an identity token.
* *
* @param message containing the heading * @param message containing the identity
* @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()
{ {
return getMessage().getSubMessage(getOffset(), getOffset() + 48); int start = getOffset() + IDENTIFIER_START;
int available = Math.max(0, getMessage().size() - start);
int length = Math.min(getVariableLengthPayloadSize() * 8, available);
return getMessage().getSubMessage(start, start + length);
} }
/** /**
* Extracts the payload contents. * Request identifier assembled big-endian from the 1-4 payload bytes. Moto.Net (LRRPPacket.cs) and node-dmr-lib
* (LRRP.js) both read the identifier this way.
* *
* Note: assumes that the values are 1-4 bytes in big-endian format? Not sure if this is correct because the * @return identifier, or zero when the payload length is not 1-4 bytes.
* 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;
while(0 < length && length <= 4) for(int x = 0; x < length; x++)
{ {
int byteValue = getMessage().getInt(BYTE_VALUE, getOffset() + payloadOffset); value = (value << 8) | getMessage().getInt(BYTE_VALUE, getOffset() + IDENTIFIER_START + (x * 8));
byteValue = Integer.rotateLeft(byteValue, payloadOffset - 16);
value += byteValue;
payloadOffset += 8;
length--;
} }
return value; return value;
} }
@Override @Override
public String toString() public String toString()
{ {
int id = getID(); //Hexadecimal, as transmitted (e.g. ID:FFFFEE)
return "ID:" + getIdentifierBytes().toHexString();
if(id != 0)
{
return "ID:" + id;
}
return "ID:" + getTokenContents().toHexString();
} }
} }

View File

@@ -0,0 +1,67 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP request parameter of unknown meaning that carries a 7-bit variable length integer value.
*
* Start Tokens: 0x42 and 0x44
* Total Length: 1 byte identifier plus a variable length integer value (0-5 bytes)
*
* Observed in Motorola P25 triggered location start requests as 0x44 0x64 (100) and 0x42 0x9C 0x10 (3600), placed
* among the requested-field flags. Moto.Net emits a bare 0x42 as a "trigger on GPIO" flag, so the value is treated
* as optional and the token is rendered without a value when none follows.
*/
public class RequestParameter extends VarIntToken
{
private TokenType mTokenType;
/**
* Constructs an instance of a request parameter token.
*
* @param tokenType of the parameter (REQUEST_PARAMETER_42 or REQUEST_PARAMETER_44)
* @param message containing the token
* @param offset to the start of the token
* @param remainingCharacterCount number of payload bytes remaining, counting from the token identifier
*/
public RequestParameter(TokenType tokenType, CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
{
super(message, offset, remainingCharacterCount);
mTokenType = tokenType;
}
@Override
public TokenType getTokenType()
{
return mTokenType;
}
@Override
public String toString()
{
if(hasValue())
{
return getTokenType() + ":" + getValue();
}
return getTokenType().toString();
}
}

View File

@@ -63,12 +63,28 @@ 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(); return !mRequestedTokens.isEmpty() || !mUnrecognizedRequestedTokens.isEmpty();
} }
@Override @Override

View File

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

View File

@@ -88,6 +88,11 @@ 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

View File

@@ -26,10 +26,32 @@ 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)
{ {
TokenType tokenType = TokenType.fromValue(id); return createToken(TokenType.fromValue(id), message, offset, remainingCharacterCount);
}
/**
* 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:
@@ -52,14 +74,17 @@ 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); return new TriggerDistance(message, offset, remainingCharacterCount);
case TRIGGER_GPIO: case TRIGGER_INTERVAL:
return new TriggerGpio(message, offset); return new TriggerInterval(message, offset, remainingCharacterCount);
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:

View File

@@ -26,21 +26,37 @@ import java.util.Map;
/** /**
* LRRP Token Types * LRRP Token Types
* *
* Variable length tokens consist of an identifier, a length indicator, and then a sequence of bytes equal in length * Fixed length tokens consist of an identifier followed by a fixed number of value bytes. Variable length tokens
* to the length indicator. * consist of an identifier, a length indicator, and then a sequence of bytes equal in length to the length indicator.
* Varint tokens consist of an identifier followed by a 7-bit variable length integer (see VarInt).
*
* A few identifiers have a different meaning in a request than in a response (0x34 is the periodic trigger flag in a
* request and the timestamp in a response). Use fromRequestValue() for request packets and fromValue() otherwise.
*
* Note: the declaration order (ordinal) controls the order that tokens are listed when a packet is rendered as text.
*/ */
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"),
@@ -48,37 +64,55 @@ 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"),
REQUEST_61("61", 1, "REQUEST-61"), HORIZONTAL_SPEED("62", 0,"HORIZONTAL SPEED"),
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_DISTANCE, TRIGGER_GPIO, private static EnumSet<TokenType> REQUEST_PARAMETER_TOKENS = EnumSet.of(IDENTITY, TRIGGER_PERIODIC,
TRIGGER_ON_MOVE, TRIGGER_PERIODIC, REQUEST_61, REQUEST_73); TRIGGER_INTERVAL, TRIGGER_CONDITION, TRIGGER_DISTANCE, REQUEST_PARAMETER_42, REQUEST_PARAMETER_44,
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. * @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
*/ */
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;
} }
/** /**
@@ -94,7 +128,23 @@ public enum TokenType
*/ */
public boolean isVariableLength() public boolean isVariableLength()
{ {
return mLength == -1; return mLength == VARIABLE_LENGTH;
}
/**
* Indicates if this token's value is a 7-bit variable length integer (varint).
*/
public boolean isVarInt()
{
return mLength == VARINT_LENGTH;
}
/**
* Indicates if this meaning of the token value applies to request packets only.
*/
public boolean isRequestOnly()
{
return mRequestOnly;
} }
/** /**
@@ -116,17 +166,26 @@ 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())
{ {
LOOKUP_MAP.put(tokenType.getValue(), tokenType); if(tokenType.isRequestOnly())
{
REQUEST_LOOKUP_MAP.put(tokenType.getValue(), tokenType);
}
else
{
LOOKUP_MAP.put(tokenType.getValue(), tokenType);
REQUEST_LOOKUP_MAP.putIfAbsent(tokenType.getValue(), tokenType);
}
} }
} }
/** /**
* Lookup a token from a 2-character string value * Lookup a token from a 2-character string value, using the response packet meaning of the value.
* @param value that represents the token * @param value that represents the token
* @return matching token or UNKNOWN. * @return matching token or UNKNOWN.
*/ */
@@ -140,6 +199,21 @@ 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()
{ {

View File

@@ -0,0 +1,135 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* LRRP Trigger Condition Token
*
* Start Token: 0x4A
* Total Length: 2 bytes
*
* Event that causes the radio to send a triggered location report. Condition values are from node-dmr-lib
* (src/DMRConst.js) and match captured requests: 0x4A 0x02 emergency, 0x4A 0x42 pin status change (telemetry),
* 0x4A 0x50 PTT and 0x4A 0x51 PTT emergency.
*/
public class TriggerCondition extends Token
{
private static final int[] CONDITION = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**
* Trigger condition values
*/
public enum Condition
{
EMERGENCY(0x02, "EMERGENCY"),
PIN_STATUS_CHANGE(0x42, "PIN STATUS CHANGE"),
PTT(0x50, "PTT"),
PTT_EMERGENCY(0x51, "PTT EMERGENCY"),
UNKNOWN(-1, "UNKNOWN");
private int mValue;
private String mLabel;
Condition(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Numeric value of the condition
*/
public int getValue()
{
return mValue;
}
/**
* Lookup a condition from a value
* @param value to lookup
* @return matching condition or UNKNOWN
*/
public static Condition fromValue(int value)
{
for(Condition condition: Condition.values())
{
if(condition.getValue() == value)
{
return condition;
}
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}
/**
* Constructs an instance of a trigger condition token.
*
* @param message containing the token
* @param offset to the start of the token
*/
public TriggerCondition(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
@Override
public TokenType getTokenType()
{
return TokenType.TRIGGER_CONDITION;
}
/**
* Raw condition value
*/
public int getConditionValue()
{
return getMessage().getInt(CONDITION, getOffset());
}
/**
* Trigger condition
*/
public Condition getCondition()
{
return Condition.fromValue(getConditionValue());
}
@Override
public String toString()
{
Condition condition = getCondition();
if(condition == Condition.UNKNOWN)
{
return "TRIGGER CONDITION:UNKNOWN[0x" + String.format("%02X", getConditionValue()) + "]";
}
return "TRIGGER CONDITION:" + condition;
}
}

View File

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

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer * Copyright (C) 2014-2026 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,38 +16,49 @@
* 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 on GPIO Token * LRRP Trigger Interval Token
* *
* Total Length: 1 byte * Start Token: 0x31
* Total Length: 1 byte identifier plus a 7-bit variable length integer value (1-5 bytes)
*
* Reporting interval in seconds for a periodic triggered location request, e.g. 0x31 0x84 0x58 = 600 seconds.
*/ */
public class TriggerGpio extends Token public class TriggerInterval extends VarIntToken
{ {
/** /**
* Constructs an instance of a heading token. * Constructs an instance of a trigger interval token.
* *
* @param message containing the heading * @param message containing the token
* @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 TriggerGpio(CorrectedBinaryMessage message, int offset) public TriggerInterval(CorrectedBinaryMessage message, int offset, int remainingCharacterCount)
{ {
super(message, offset); super(message, offset, remainingCharacterCount);
} }
@Override @Override
public TokenType getTokenType() public TokenType getTokenType()
{ {
return TokenType.TRIGGER_GPIO; return TokenType.TRIGGER_INTERVAL;
}
/**
* Interval in seconds
*/
public int getSeconds()
{
return getValue();
} }
@Override @Override
public String toString() public String toString()
{ {
return "TRIGGER ON GPIO"; return "INTERVAL:" + getSeconds() + " SECS";
} }
} }

View File

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

View File

@@ -0,0 +1,105 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token;
import io.github.dsheirer.bits.BinaryMessage;
/**
* LRRP variable length integer (varint) reader.
*
* LRRP encodes integer token values as one or more bytes where each byte carries seven value bits, most significant
* group first, and the high bit of a byte indicates that another byte follows. For example 0x84 0x58 decodes to
* (0x04 << 7) | 0x58 = 600, 0x9C 0x10 decodes to (0x1C << 7) | 0x10 = 3600 and a value smaller than 128 occupies a
* single byte (0x64 = 100).
*
* References:
* - Moto.Net, LRRPPacket.cs: "looks like this stuff is using VLQ ... for most of the fields"
* - node-dmr-lib, src/Protocols/LRRP.js: parseLRRPInt() and toLRRPInt()
*/
public final class VarInt
{
/**
* Maximum number of bytes consumed for a single value. Five 7-bit groups hold 35 bits, which covers any 32-bit
* value, and the cap keeps a corrupted run of continuation bits from consuming the rest of a packet.
*/
public static final int MAX_BYTES = 5;
private static final int[] BYTE_VALUE = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int CONTINUATION_MASK = 0x80;
private static final int VALUE_MASK = 0x7F;
private VarInt()
{
//Static utility class
}
/**
* Number of bytes occupied by the varint that starts at the bit offset.
*
* @param message containing the value
* @param offset bit offset to the first byte of the value
* @param limit maximum number of bytes available for the value (ie bytes remaining in the packet payload)
* @return byte count from 0 to the lesser of limit and MAX_BYTES. Zero indicates that no value is present.
*/
public static int length(BinaryMessage message, int offset, int limit)
{
int maximum = Math.min(limit, MAX_BYTES);
int count = 0;
while(count < maximum)
{
int b = message.getInt(BYTE_VALUE, offset + (count * 8));
count++;
if((b & CONTINUATION_MASK) == 0)
{
break;
}
}
return count;
}
/**
* Decodes the varint that starts at the bit offset.
*
* @param message containing the value
* @param offset bit offset to the first byte of the value
* @param limit maximum number of bytes available for the value (ie bytes remaining in the packet payload)
* @return decoded value, or zero when no bytes are available.
*/
public static int value(BinaryMessage message, int offset, int limit)
{
int maximum = Math.min(limit, MAX_BYTES);
int value = 0;
for(int count = 0; count < maximum; count++)
{
int b = message.getInt(BYTE_VALUE, offset + (count * 8));
value = (value << 7) | (b & VALUE_MASK);
if((b & CONTINUATION_MASK) == 0)
{
break;
}
}
return value;
}
}

View File

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

View File

@@ -101,7 +101,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCStatusUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCStatusUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCTelephoneInterconnectChannelGrant; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCTelephoneInterconnectChannelGrant;
@@ -193,6 +192,14 @@ 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
@@ -420,6 +427,12 @@ 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())
{ {
@@ -599,6 +612,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
//Network configuration messages //Network configuration messages
case OSP_ADJACENT_STATUS_BROADCAST: case OSP_ADJACENT_STATUS_BROADCAST:
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
mNetworkConfigurationMonitor.process(ambtc); mNetworkConfigurationMonitor.process(ambtc);
break; break;
case OSP_NETWORK_STATUS_BROADCAST: case OSP_NETWORK_STATUS_BROADCAST:
@@ -678,14 +692,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
mu.getShortDataMessage()); mu.getShortDataMessage());
} }
break; break;
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
if(ambtc instanceof AMBTCProtectionParameterBroadcast ppb)
{
broadcastEvent(ambtc, DecodeEventType.RESPONSE, "USE ENCRYPTION " + ppb.getEncryptionKey() +
" OUTBOUND MI:" + ppb.getOutboundMessageIndicator() +
" INBOUND MI:" + ppb.getInboundMessageIndicator());
}
break;
case OSP_ROAMING_ADDRESS_UPDATE: case OSP_ROAMING_ADDRESS_UPDATE:
broadcastEvent(ambtc.getIdentifiers(), ambtc.getTimestamp(), DecodeEventType.RESPONSE, broadcastEvent(ambtc.getIdentifiers(), ambtc.getTimestamp(), DecodeEventType.RESPONSE,
"ROAMING ADDRESS UPDATE"); "ROAMING ADDRESS UPDATE");
@@ -846,6 +852,7 @@ 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());
@@ -888,6 +895,7 @@ 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())
{ {
@@ -911,6 +919,7 @@ 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));
} }
@@ -929,6 +938,7 @@ 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);

View File

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

View File

@@ -34,7 +34,7 @@ public class HDUMessage extends P25P1Message
private final static Logger mLog = LoggerFactory.getLogger(HDUMessage.class); private 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, 278, 396, 414, 432, 450, 468, 486, 504, 522, 540, 558, 576, 594, 612, 630}; 270, 288, 306, 324, 342, 360, 378, 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};

View File

@@ -24,6 +24,7 @@ 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
{ {
@@ -41,8 +42,41 @@ 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;
}
}
} }

View File

@@ -37,6 +37,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitAnswerResponse; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitAnswerResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitVoiceServiceRequest; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitVoiceServiceRequest;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcastUncoordinated;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCCallAlert; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCCallAlert;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCFrequencyBandUpdateTDMA; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCFrequencyBandUpdateTDMA;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCGroupAffiliationQuery; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCGroupAffiliationQuery;
@@ -47,7 +48,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressResponse; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressUpdate;
@@ -173,6 +173,13 @@ 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);
@@ -229,7 +236,7 @@ public class PDUMessageFactory
} }
/** /**
* Creates a packet data message * Creates a packet data message from a confirmed or unconfirmed packet data sequence.
* *
* @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
@@ -337,7 +344,7 @@ public class PDUMessageFactory
case OSP_NETWORK_STATUS_BROADCAST: case OSP_NETWORK_STATUS_BROADCAST:
return new AMBTCNetworkStatusBroadcast(pduSequence, nac, timestamp); return new AMBTCNetworkStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN: case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
return new AMBTCProtectionParameterBroadcast(pduSequence, nac, timestamp); return new AMBTCAdjacentStatusBroadcastUncoordinated(pduSequence, nac, timestamp);
case OSP_RFSS_STATUS_BROADCAST: case OSP_RFSS_STATUS_BROADCAST:
return new AMBTCRFSSStatusBroadcast(pduSequence, nac, timestamp); return new AMBTCRFSSStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ROAMING_ADDRESS_UPDATE: case OSP_ROAMING_ADDRESS_UPDATE:

View File

@@ -0,0 +1,146 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.APCO25Wacn;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25ExplicitChannel;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.block.UnconfirmedDataBlock;
import io.github.dsheirer.module.decode.p25.reference.SystemServiceClass;
/**
* Adjacent Status Broadcast - Uncoordinated Band Plan (opcode 0x3E), extended/multi-block (AMBTC) format.
*
* Announces a neighbor site that may belong to a different WACN/system (and band plan) than the current site.
* Opcode 0x3E was previously defined as Protection Parameter Broadcast; current TIA-102.AABC revisions reassign it.
*
* Header: LRA (24-31), flags (32-35), SYSTEM (36-47), RFSS (64-71), SITE (72-79) -- same as opcode 0x3C
* Block 0: TRANSMIT CHANNEL (0-15), RECEIVE CHANNEL (16-31), SYSTEM SERVICE CLASS (32-39), WACN (40-59)
*
* A receive channel of 0xFFFF indicates that no separate receive channel is specified.
*/
public class AMBTCAdjacentStatusBroadcastUncoordinated extends AMBTCAdjacentStatusBroadcast
{
private static final int[] BLOCK_0_DOWNLINK_FREQUENCY_BAND = {0, 1, 2, 3};
private static final int[] BLOCK_0_DOWNLINK_CHANNEL_NUMBER = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] BLOCK_0_UPLINK_CHANNEL = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_UPLINK_FREQUENCY_BAND = {16, 17, 18, 19};
private static final int[] BLOCK_0_UPLINK_CHANNEL_NUMBER = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_SYSTEM_SERVICE_CLASS = {32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] BLOCK_0_WACN = {40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59};
private static final int UNSPECIFIED_CHANNEL = 0xFFFF;
private IBandChannelDescriptor mChannel;
private Identifier mWacn;
private SystemServiceClass mSystemServiceClass;
public AMBTCAdjacentStatusBroadcastUncoordinated(PDUSequence PDUSequence, int nac, long timestamp)
{
super(PDUSequence, nac, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" WACN:").append(getWacn());
sb.append(" SYSTEM:").append(getSystem());
sb.append(" LRA:").append(getLocationRegistrationArea());
sb.append(" RFSS:").append(getRfss());
sb.append(" SITE:").append(getSite());
sb.append(" CHANNEL:").append(getChannel());
sb.append(" SERVICES:").append(getSystemServiceClass());
return sb.toString();
}
/**
* WACN of the neighbor site
*/
public Identifier getWacn()
{
if(mWacn == null && hasDataBlock(0))
{
mWacn = APCO25Wacn.create(getDataBlock(0).getMessage().getInt(BLOCK_0_WACN));
}
return mWacn;
}
/**
* Services offered by the neighbor site
*/
public SystemServiceClass getSystemServiceClass()
{
if(mSystemServiceClass == null)
{
mSystemServiceClass = new SystemServiceClass(hasDataBlock(0) ?
getDataBlock(0).getMessage().getInt(BLOCK_0_SYSTEM_SERVICE_CLASS) : 0);
}
return mSystemServiceClass;
}
private boolean hasUplinkChannel()
{
return hasDataBlock(0) && getDataBlock(0).getMessage().getInt(BLOCK_0_UPLINK_CHANNEL) != UNSPECIFIED_CHANNEL;
}
@Override
public boolean isExtendedChannel()
{
return hasUplinkChannel() && super.isExtendedChannel();
}
@Override
public IBandChannelDescriptor getChannel()
{
if(mChannel == null)
{
if(hasDataBlock(0))
{
UnconfirmedDataBlock block = getDataBlock(0);
if(isExtendedChannel())
{
mChannel = APCO25ExplicitChannel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER),
block.getMessage().getInt(BLOCK_0_UPLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_UPLINK_CHANNEL_NUMBER));
}
else
{
mChannel = APCO25Channel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER));
}
}
else
{
mChannel = APCO25Channel.create(-1, 0);
}
}
return mChannel;
}
}

View File

@@ -80,4 +80,13 @@ 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);
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer * Copyright (C) 2014-2026 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,6 +247,7 @@ 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"));

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer * Copyright (C) 2014-2026 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,6 +48,7 @@ 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;
@@ -217,12 +218,18 @@ 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
{ {

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,19 +32,13 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager; import io.github.dsheirer.source.tuner.manager.UsbDeviceLookup;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.usb4java.Context;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.DeviceHandle;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
/** /**
* Airspy tuner controller, implemented with the jAirspy library (libairspy). * Airspy tuner controller, implemented with the jAirspy library (libairspy).
@@ -183,13 +177,22 @@ public class AirspyTunerController extends TunerController
} }
/** /**
* Closes the device, which also stops streaming. * Turns off the bias-T and closes the device, which also stops streaming.
* @param device to close, or null * @param device to close, or null
*/ */
private static void closeDevice(AirspyDevice device) private static void closeDevice(AirspyDevice device)
{ {
if(device != null) if(device != null)
{ {
try
{
device.setBiasTee(false);
}
catch(AirspyException | RuntimeException e)
{
mLog.warn("Error turning off Airspy bias-T before closing the device - " + e.getMessage());
}
try try
{ {
device.close(); device.close();
@@ -203,98 +206,17 @@ public class AirspyTunerController extends TunerController
/** /**
* Reads the serial number of the device at this controller's USB bus and port, since libairspy opens a specific * Reads the serial number of the device at this controller's USB bus and port, since libairspy opens a specific
* device by serial number. The device is closed again before returning so that libairspy can open it. * device by serial number. The USB serial string is formatted as: AIRSPY SN:0123456789ABCDEF
* @return serial number * @return serial number
* @throws SourceException if the device isn't found or can't be accessed. * @throws SourceException if the device isn't found or can't be accessed.
*/ */
private long readSerialNumber() throws SourceException private long readSerialNumber() throws SourceException
{ {
Context context = new Context(); String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
int status = LibUsb.init(context);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
try try
{ {
DeviceList deviceList = new DeviceList(); return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1).trim(), 16);
int count = LibUsb.getDeviceList(context, deviceList);
if(count < 0)
{
throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
}
try
{
for(Device device : deviceList)
{
if(LibUsb.getBusNumber(device) == mBus && LibUsb.getPortNumber(device) > 0 &&
mPortAddress != null && mPortAddress.equals(TunerManager.getPortAddress(device)))
{
return readSerialNumber(device);
}
}
}
finally
{
LibUsb.freeDeviceList(deviceList, true);
}
}
finally
{
LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
}
/**
* Reads the serial number from the device's USB serial string, formatted as: AIRSPY SN:0123456789ABCDEF
* @param device to read
* @return serial number
* @throws SourceException if the device can't be opened or the serial string isn't valid.
*/
private static long readSerialNumber(Device device) throws SourceException
{
DeviceDescriptor descriptor = new DeviceDescriptor();
int status = LibUsb.getDeviceDescriptor(device, descriptor);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
}
DeviceHandle handle = new DeviceHandle();
status = LibUsb.open(device, handle);
if(status == LibUsb.ERROR_ACCESS)
{
mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
}
else if(status != LibUsb.SUCCESS)
{
mLog.error("Can't open USB tuner - check driver or Linux udev rules");
throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
}
String serial;
try
{
serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
}
finally
{
LibUsb.close(handle);
}
try
{
return Long.parseUnsignedLong(serial.substring(serial.lastIndexOf(':') + 1), 16);
} }
catch(NullPointerException | NumberFormatException e) catch(NullPointerException | NumberFormatException e)
{ {
@@ -462,6 +384,7 @@ public class AirspyTunerController extends TunerController
setLNAGain(config.getLNAGain()); setLNAGain(config.getLNAGain());
setMixerAGC(config.isMixerAGC()); setMixerAGC(config.isMixerAGC());
setLNAAGC(config.isLNAAGC()); setLNAAGC(config.isLNAAGC());
setBiasT(config.isBiasT());
//Set the gain mode last, so custom values are already set, and linearity and sensitivity modes will //Set the gain mode last, so custom values are already set, and linearity and sensitivity modes will
//automatically override the custom values. //automatically override the custom values.
@@ -620,6 +543,17 @@ public class AirspyTunerController extends TunerController
execute(device -> device.setLnaAgc(enabled)); execute(device -> device.setLnaAgc(enabled));
} }
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
execute(device -> device.setBiasTee(enabled));
}
public void setGain(Gain gain) throws SourceException public void setGain(Gain gain) throws SourceException
{ {
if(gain != Gain.CUSTOM) if(gain != Gain.CUSTOM)

View File

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

View File

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

View File

@@ -32,18 +32,12 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager; import io.github.dsheirer.source.tuner.manager.UsbDeviceLookup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.usb4java.Context;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.DeviceHandle;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
/** /**
* Airspy HF+ Dual Port, HF+ Discovery and Ranger tuner controller, implemented with the jAirspyHF library * Airspy HF+ Dual Port, HF+ Discovery and Ranger tuner controller, implemented with the jAirspyHF library
@@ -73,6 +67,8 @@ public class AirspyHfTunerController extends TunerController
private Attenuation mAttenuation = Attenuation.A0; private Attenuation mAttenuation = Attenuation.A0;
private boolean mAgcEnabled; private boolean mAgcEnabled;
private boolean mLnaEnabled; private boolean mLnaEnabled;
private boolean mBiasTSupported;
private boolean mBiasTEnabled;
private volatile float mSamplesPerMillisecond = DEFAULT_SAMPLE_RATE.getSampleRate() / 1000.0f; private volatile float mSamplesPerMillisecond = DEFAULT_SAMPLE_RATE.getSampleRate() / 1000.0f;
private volatile int mBufferSampleCount = DEFAULT_BUFFER_SAMPLE_COUNT; private volatile int mBufferSampleCount = DEFAULT_BUFFER_SAMPLE_COUNT;
private boolean mStreaming = false; private boolean mStreaming = false;
@@ -116,6 +112,8 @@ public class AirspyHfTunerController extends TunerController
try try
{ {
mBiasTSupported = false;
mBiasTEnabled = false;
mDevice = AirspyHf.open(serialNumber); mDevice = AirspyHf.open(serialNumber);
BoardInfo boardInfo = mDevice.boardInfo(); BoardInfo boardInfo = mDevice.boardInfo();
@@ -139,6 +137,8 @@ public class AirspyHfTunerController extends TunerController
} }
mAvailableSampleRates = Collections.unmodifiableList(sampleRates); mAvailableSampleRates = Collections.unmodifiableList(sampleRates);
mBiasTSupported = detectBiasTSupport(mDevice);
mBiasTEnabled = false;
//Force the sample rate to be applied, since the rate discovery leaves the device at the last rate. //Force the sample rate to be applied, since the rate discovery leaves the device at the last rate.
mCurrentSampleRate = null; mCurrentSampleRate = null;
@@ -161,6 +161,7 @@ public class AirspyHfTunerController extends TunerController
public void stop() public void stop()
{ {
AirspyHfDevice device; AirspyHfDevice device;
boolean biasTEnabled;
getLock().lock(); getLock().lock();
@@ -169,6 +170,8 @@ public class AirspyHfTunerController extends TunerController
device = mDevice; device = mDevice;
mDevice = null; mDevice = null;
mStreaming = false; mStreaming = false;
biasTEnabled = mBiasTEnabled;
mBiasTEnabled = false;
} }
finally finally
{ {
@@ -176,9 +179,58 @@ public class AirspyHfTunerController extends TunerController
} }
mNativeBufferBroadcaster.clear(); mNativeBufferBroadcaster.clear();
if(device != null && biasTEnabled)
{
try
{
device.setBiasTee(0);
}
catch(AirspyHfException | RuntimeException e)
{
mLog.warn("Error turning off Airspy HF bias-T before closing the device - " + e.getMessage());
}
}
closeDevice(device); closeDevice(device);
} }
/**
* Detects if the device firmware supports a bias-T. Firmware without bias-T support rejects the bias-T commands.
* Some firmware (HF+ Discovery R5.0.1) rejects the bias-T count request, so this falls back to turning the bias-T
* off, which is a safe probe since the bias-T should be off at startup anyway.
* @param device to probe
* @return true if the device accepts bias-T commands
*/
private static boolean detectBiasTSupport(AirspyHfDevice device)
{
try
{
if(device.biasTeeCount() > 0)
{
device.setBiasTee(0);
return true;
}
return false;
}
catch(AirspyHfException | RuntimeException e)
{
//Bias-T count not supported - fall through and probe with a bias-T off command.
}
try
{
device.setBiasTee(0);
return true;
}
catch(AirspyHfException | RuntimeException e)
{
mLog.debug("Airspy HF firmware doesn't support bias-T - " + e.getMessage());
return false;
}
}
/** /**
* Closes the device, which also stops streaming. * Closes the device, which also stops streaming.
* @param device to close, or null * @param device to close, or null
@@ -200,94 +252,13 @@ public class AirspyHfTunerController extends TunerController
/** /**
* Reads the serial number of the device at this controller's USB bus and port, since libairspyhf opens a specific * Reads the serial number of the device at this controller's USB bus and port, since libairspyhf opens a specific
* device by serial number. The device is closed again before returning so that libairspyhf can open it. * device by serial number. The USB serial string is formatted as: AIRSPYHF SN:0123456789ABCDEF
* @return serial number * @return serial number
* @throws SourceException if the device isn't found or can't be accessed. * @throws SourceException if the device isn't found or can't be accessed.
*/ */
private long readSerialNumber() throws SourceException private long readSerialNumber() throws SourceException
{ {
Context context = new Context(); String serial = UsbDeviceLookup.readSerialNumber(mBus, mPortAddress);
int status = LibUsb.init(context);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
try
{
DeviceList deviceList = new DeviceList();
int count = LibUsb.getDeviceList(context, deviceList);
if(count < 0)
{
throw new SourceException("Can't get the USB device list - " + LibUsb.errorName(count));
}
try
{
for(Device device : deviceList)
{
if(LibUsb.getBusNumber(device) == mBus && LibUsb.getPortNumber(device) > 0 &&
mPortAddress != null && mPortAddress.equals(TunerManager.getPortAddress(device)))
{
return readSerialNumber(device);
}
}
}
finally
{
LibUsb.freeDeviceList(deviceList, true);
}
}
finally
{
LibUsb.exit(context);
}
throw new SourceException("Couldn't find USB device at bus [" + mBus + "] port [" + mPortAddress + "]");
}
/**
* Reads the serial number from the device's USB serial string, formatted as: AIRSPYHF SN:0123456789ABCDEF
* @param device to read
* @return serial number
* @throws SourceException if the device can't be opened or the serial string isn't valid.
*/
private static long readSerialNumber(Device device) throws SourceException
{
DeviceDescriptor descriptor = new DeviceDescriptor();
int status = LibUsb.getDeviceDescriptor(device, descriptor);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
}
DeviceHandle handle = new DeviceHandle();
status = LibUsb.open(device, handle);
if(status == LibUsb.ERROR_ACCESS)
{
mLog.error("Access to USB tuner denied - (windows) reinstall zadig driver or (linux) blacklist driver and/or check udev rules");
throw new SourceException("access denied - if using linux, blacklist the default driver and/or install udev rules");
}
else if(status != LibUsb.SUCCESS)
{
mLog.error("Can't open USB tuner - check driver or Linux udev rules");
throw new SourceException("Can't open USB tuner - reinstall driver? - " + LibUsb.errorName(status));
}
String serial;
try
{
serial = LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber());
}
finally
{
LibUsb.close(handle);
}
try try
{ {
@@ -448,6 +419,11 @@ public class AirspyHfTunerController extends TunerController
setLna(airspyConfig.isLna()); setLna(airspyConfig.isLna());
setAttenuation(airspyConfig.getAttenuation()); setAttenuation(airspyConfig.getAttenuation());
setAgc(airspyConfig.isAgc()); setAgc(airspyConfig.isAgc());
if(isBiasTSupported())
{
setBiasT(airspyConfig.isBiasT());
}
} }
catch(SourceException se) catch(SourceException se)
{ {
@@ -670,4 +646,38 @@ public class AirspyHfTunerController extends TunerController
execute(device -> device.setAttenuationStep(attenuation.getValue())); execute(device -> device.setAttenuationStep(attenuation.getValue()));
mAttenuation = attenuation; mAttenuation = attenuation;
} }
/**
* Indicates if the device firmware supports a bias-T, available once the tuner is started.
* @return true if supported
*/
public boolean isBiasTSupported()
{
return mBiasTSupported;
}
/**
* Indicates if the Bias-T power output is enabled.
* @return enabled state
*/
public boolean getBiasT()
{
return mBiasTEnabled;
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException if the device doesn't support a bias-T or there is an error
*/
public void setBiasT(boolean enabled) throws SourceException
{
if(!mBiasTSupported)
{
throw new SourceException("Airspy HF firmware doesn't support bias-T");
}
execute(device -> device.setBiasTee(enabled ? 1 : 0));
mBiasTEnabled = enabled;
}
} }

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ package io.github.dsheirer.source.tuner.fcd;
public enum FCDCommand public enum FCDCommand
{ {
/** /**
* Note: the javaxUSB and usb4Java libraries are slightly different than * Note: the libusb interrupt transfers used here (through jlibusb) are slightly different than
* the signal9 HID libraries used in C, so you will see variations in the * the signal9 HID libraries used in C, so you will see variations in the
* byte array length and indexes, when comparing this setup to the qthid * byte array length and indexes, when comparing this setup to the qthid
* source code. * source code.

View File

@@ -19,6 +19,11 @@
package io.github.dsheirer.source.tuner.fcd; package io.github.dsheirer.source.tuner.fcd;
import io.github.dsheirer.buffer.INativeBuffer; import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.libusb.LibUsb;
import io.github.dsheirer.libusb.UsbContext;
import io.github.dsheirer.libusb.UsbDeviceHandle;
import io.github.dsheirer.libusb.UsbDeviceInfo;
import io.github.dsheirer.libusb.UsbException;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.adapter.ComplexShortAdapter; import io.github.dsheirer.sample.adapter.ComplexShortAdapter;
import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.SourceException;
@@ -28,21 +33,13 @@ import io.github.dsheirer.source.tuner.MixerTunerType;
import io.github.dsheirer.source.tuner.TunerClass; import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.TunerType; import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.manager.TunerManager; import io.github.dsheirer.source.tuner.manager.UsbDeviceLookup;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.usb4java.Context;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.DeviceHandle;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;
import javax.sound.sampled.TargetDataLine; import javax.sound.sampled.TargetDataLine;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public abstract class FCDTunerController extends TunerController public abstract class FCDTunerController extends TunerController
@@ -50,16 +47,19 @@ public abstract class FCDTunerController extends TunerController
private final static Logger mLog = LoggerFactory.getLogger(FCDTunerController.class); private final static Logger mLog = LoggerFactory.getLogger(FCDTunerController.class);
private static final double USABLE_BANDWIDTH_PERCENT = 1.0; private static final double USABLE_BANDWIDTH_PERCENT = 1.0;
private static final int DC_SPIKE_AVOID_HALF_BANDWIDTH = 5000; private static final int DC_SPIKE_AVOID_HALF_BANDWIDTH = 5000;
private final static byte FCD_INTERFACE = (byte) 0x2; private final static int FCD_INTERFACE = 0x2;
private final static byte FCD_ENDPOINT_IN = (byte) 0x82; private final static int FCD_ENDPOINT_IN = 0x82;
private final static byte FCD_ENDPOINT_OUT = (byte) 0x2; private final static int FCD_ENDPOINT_OUT = 0x2;
private final static int FCD_PACKET_SIZE = 64;
private final static int FCD_TRANSFER_TIMEOUT_MS = 500;
private final static int LIBUSB_ERROR_NO_DEVICE = -4;
private int mBus; private int mBus;
private String mPortAddress; private String mPortAddress;
private Context mDeviceContext = new Context(); private UsbContext mUsbContext;
private Device mDevice; private UsbDeviceInfo mDevice;
private DeviceDescriptor mDeviceDescriptor = new DeviceDescriptor(); private volatile UsbDeviceHandle mDeviceHandle;
private DeviceHandle mDeviceHandle = new DeviceHandle(); private boolean mShutdown = false;
private FCDConfiguration mConfiguration = new FCDConfiguration(); private FCDConfiguration mConfiguration = new FCDConfiguration();
protected ComplexMixer mComplexMixer; protected ComplexMixer mComplexMixer;
@@ -168,58 +168,6 @@ public abstract class FCDTunerController extends TunerController
} }
} }
/**
* Finds the USB device for this tuner at the specified USB bus and port.
* @return discovered USB device
* @throws SourceException if there is an error or the device is not discovered.
*/
private Device findDevice() throws SourceException
{
Device foundDevice = null;
DeviceList deviceList = new DeviceList();
int count = LibUsb.getDeviceList(mDeviceContext, deviceList);
if(count >= 0)
{
for(Device device: deviceList)
{
int bus = LibUsb.getBusNumber(device);
int port = LibUsb.getPortNumber(device);
if(port > 0)
{
String portAddress = TunerManager.getPortAddress(device);
if(mBus == bus && mPortAddress != null && mPortAddress.equals(portAddress))
{
foundDevice = device;
}
else
{
LibUsb.unrefDevice(device);
}
}
else
{
LibUsb.unrefDevice(device);
}
}
}
else
{
throw new SourceException("LibUsb couldn't discover USB device [" + mBus + ":" + mPortAddress +
"] from device list" + (count < 0 ? " - error: " + LibUsb.errorName(count) : ""));
}
if(foundDevice == null)
{
throw new SourceException("LibUsb couldn't find the matching USB device");
}
return foundDevice;
}
/** /**
* Initializes the controller by opening the USB device and claiming the HID interface. * Initializes the controller by opening the USB device and claiming the HID interface.
* Invoke this method after constructing this class to setup the controller. * Invoke this method after constructing this class to setup the controller.
@@ -228,80 +176,102 @@ public abstract class FCDTunerController extends TunerController
*/ */
public void start() throws SourceException public void start() throws SourceException
{ {
if(mDeviceContext == null) if(mShutdown)
{ {
throw new SourceException("Device cannot be reused once it has been shutdown"); throw new SourceException("Device cannot be reused once it has been shutdown");
} }
int status = LibUsb.init(mDeviceContext); mUsbContext = UsbDeviceLookup.createContext();
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't initialize libusb library - " + LibUsb.errorName(status));
}
mDevice = findDevice();
mDeviceDescriptor = new DeviceDescriptor();
status = LibUsb.getDeviceDescriptor(mDevice, mDeviceDescriptor);
if(status != LibUsb.SUCCESS)
{
mDeviceDescriptor = null;
throw new SourceException("Can't obtain tuner's device descriptor - " + LibUsb.errorName(status));
}
mDeviceHandle = new DeviceHandle();
status = LibUsb.open(mDevice, mDeviceHandle);
if(status != LibUsb.SUCCESS)
{
mDeviceHandle = null;
mDeviceDescriptor = null;
mLog.error("Can't open USB tuner [" + getTunerType() + "] - check driver or Linux udev rules");
throw new SourceException("Can't open USB tuner - check driver or Linux udev rules - " + LibUsb.errorName(status));
}
//Remove a kernel driver if active
status = LibUsb.kernelDriverActive(mDeviceHandle, FCD_INTERFACE);
if(status == 1)
{
status = LibUsb.detachKernelDriver(mDeviceHandle, FCD_INTERFACE);
if(status != LibUsb.SUCCESS)
{
mLog.error("Unable to detach kernel driver for USB tuner device - bus:" + mBus + " port:" + mPortAddress);
mDeviceHandle = null;
mDeviceDescriptor = null;
throw new SourceException("Can't detach kernel driver");
}
}
//Claim the interface
status = LibUsb.claimInterface(mDeviceHandle, FCD_INTERFACE);
if(status == LibUsb.ERROR_BUSY)
{
mDeviceHandle = null;
mDeviceDescriptor = null;
throw new SourceException("USB tuner is in-use by another application");
}
else if(status != LibUsb.SUCCESS)
{
mDeviceHandle = null;
mDeviceDescriptor = null;
throw new SourceException("Can't claim interface on USB tuner - " + LibUsb.errorName(status));
}
try try
{ {
deviceStart(); mDevice = UsbDeviceLookup.find(mUsbContext, mBus, mPortAddress);
try
{
mDeviceHandle = mUsbContext.open(mDevice);
}
catch(UsbException e)
{
mLog.error("Can't open USB tuner [" + getTunerType() + "] - check driver or Linux udev rules");
throw new SourceException("Can't open USB tuner - check driver or Linux udev rules - " +
LibUsb.errorName(e.code()));
}
//Remove a kernel driver if active
boolean kernelDriverActive;
try
{
kernelDriverActive = mDeviceHandle.isKernelDriverActive(FCD_INTERFACE);
}
catch(UsbException e)
{
kernelDriverActive = false;
}
if(kernelDriverActive)
{
try
{
mDeviceHandle.detachKernelDriver(FCD_INTERFACE);
}
catch(UsbException e)
{
mLog.error("Unable to detach kernel driver for USB tuner device - bus:" + mBus + " port:" + mPortAddress);
throw new SourceException("Can't detach kernel driver");
}
}
//Claim the interface
try
{
mDeviceHandle.claimInterface(FCD_INTERFACE);
}
catch(UsbException e)
{
if(e.code() == UsbDeviceLookup.ERROR_BUSY)
{
throw new SourceException("USB tuner is in-use by another application");
}
throw new SourceException("Can't claim interface on USB tuner - " + LibUsb.errorName(e.code()));
}
} }
catch(SourceException se) catch(SourceException se)
{ {
closeUsb();
throw se; throw se;
} }
deviceStart();
}
/**
* Closes the USB device handle and the libusb context.
*/
private void closeUsb()
{
UsbDeviceHandle handle = mDeviceHandle;
mDeviceHandle = null;
if(handle != null)
{
try
{
handle.close();
}
catch(Exception e)
{
mLog.error("error while closing device handle", e);
}
}
if(mUsbContext != null)
{
mUsbContext.close();
mUsbContext = null;
}
} }
/** /**
@@ -321,23 +291,9 @@ public abstract class FCDTunerController extends TunerController
mComplexMixer = null; mComplexMixer = null;
} }
if(mDeviceHandle != null) closeUsb();
{
try
{
LibUsb.close(mDeviceHandle);
}
catch(Exception e)
{
mLog.error("error while closing device handle", e);
}
}
mDeviceDescriptor = null;
mDeviceHandle = null;
mDevice = null; mDevice = null;
LibUsb.exit(mDeviceContext); mShutdown = true;
mDeviceContext = null;
} }
/** /**
@@ -360,19 +316,14 @@ public abstract class FCDTunerController extends TunerController
*/ */
public String getUSBAddress() public String getUSBAddress()
{ {
if(mDevice != null) UsbDeviceInfo device = mDevice;
//This is the tuner's unique ID for saved tuner configurations, so the format must not change: the port is the
//last port number in the path from the root hub, as libusb_get_port_number() reports it.
if(device != null)
{ {
StringBuilder sb = new StringBuilder(); int port = device.portNumbers().isEmpty() ? 0 : device.portNumbers().getLast();
return "Bus:" + device.busNumber() + " Port:" + port;
sb.append("Bus:");
int bus = LibUsb.getBusNumber(mDevice);
sb.append(bus);
sb.append(" Port:");
int port = LibUsb.getPortNumber(mDevice);
sb.append(port);
return sb.toString();
} }
return "UNKNOWN"; return "UNKNOWN";
@@ -383,38 +334,34 @@ public abstract class FCDTunerController extends TunerController
*/ */
public String getUSBID() public String getUSBID()
{ {
if(mDeviceDescriptor != null) UsbDeviceInfo device = mDevice;
if(device != null)
{ {
StringBuilder sb = new StringBuilder(); return String.format("%04X:%04X", device.vendorId(), device.productId());
sb.append(String.format("%04X", (int) (mDeviceDescriptor.idVendor() & 0xFFFF)));
sb.append(":");
sb.append(String.format("%04X", (int) (mDeviceDescriptor.idProduct() & 0xFFFF)));
return sb.toString();
} }
return "UNKNOWN"; return "UNKNOWN";
} }
/** /**
* USB Port Speed. Should be 2.0 for both types of funcube dongles * USB Port Speed. The FUNcube Dongle Pro is a full speed (USB 1.1) device.
*/ */
public String getUSBSpeed() public String getUSBSpeed()
{ {
if(mDevice != null) UsbDeviceInfo device = mDevice;
{
int speed = LibUsb.getDeviceSpeed(mDevice);
switch(speed) if(device != null)
{
switch(device.speed())
{ {
case 0: case LOW:
return "1.1 LOW"; return "1.1 LOW";
case 1: case FULL:
return "1.1 FULL"; return "1.1 FULL";
case 2: case HIGH:
return "2.0 HIGH"; return "2.0 HIGH";
case 3: case SUPER:
return "3.0 SUPER"; return "3.0 SUPER";
default: default:
} }
@@ -500,26 +447,27 @@ public abstract class FCDTunerController extends TunerController
} }
/** /**
* Performs an interrupt write to the OUT endpoint. * Performs an interrupt write of one 64 byte packet to the OUT endpoint.
* *
* @param buffer - direct allocated buffer. Must be 64 bytes in length. * @param packet to write. Must be 64 bytes in length.
* @throws LibUsbException on error * @throws FCDUsbException on error
*/ */
private void write(ByteBuffer buffer) throws LibUsbException private void write(byte[] packet) throws FCDUsbException
{ {
if(mDeviceHandle != null) UsbDeviceHandle handle = mDeviceHandle;
{
IntBuffer transferred = IntBuffer.allocate(1);
int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_OUT, buffer, transferred, 500l);
if(result != LibUsb.SUCCESS) if(handle == null)
{
throw new LibUsbException("error writing byte buffer", result);
}
}
else
{ {
throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE); throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
}
try
{
handle.interruptTransfer(FCD_ENDPOINT_OUT, packet, packet.length, FCD_TRANSFER_TIMEOUT_MS);
}
catch(UsbException e)
{
throw new FCDUsbException("error writing byte buffer", e.code());
} }
} }
@@ -527,14 +475,14 @@ public abstract class FCDTunerController extends TunerController
* Performs an interrupt write to the OUT endpoint for the FCD command. * Performs an interrupt write to the OUT endpoint for the FCD command.
* *
* @param command - no-argument command to write * @param command - no-argument command to write
* @throws LibUsbException - on error * @throws FCDUsbException - on error
*/ */
private void write(FCDCommand command) throws LibUsbException private void write(FCDCommand command) throws FCDUsbException
{ {
ByteBuffer buffer = ByteBuffer.allocateDirect(64); byte[] packet = new byte[FCD_PACKET_SIZE];
buffer.put(0, command.getCommand()); packet[0] = command.getCommand();
buffer.put(1, (byte) 0x00); packet[1] = (byte) 0x00;
write(buffer); write(packet);
} }
/** /**
@@ -542,46 +490,53 @@ public abstract class FCDTunerController extends TunerController
* *
* @param command - command to write * @param command - command to write
* @param argument - value to write with the command * @param argument - value to write with the command
* @throws LibUsbException - on error * @throws FCDUsbException - on error
*/ */
private void write(FCDCommand command, long argument) throws LibUsbException private void write(FCDCommand command, long argument) throws FCDUsbException
{ {
ByteBuffer buffer = ByteBuffer.allocateDirect(64); byte[] packet = new byte[FCD_PACKET_SIZE];
ByteBuffer buffer = ByteBuffer.wrap(packet);
/* The FCD expects little-endian formatted values */ /* The FCD expects little-endian formatted values */
buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(0, command.getCommand()); buffer.put(0, command.getCommand());
buffer.putLong(1, argument); buffer.putLong(1, argument);
write(buffer); write(packet);
} }
/** /**
* Performs an interrupt read against the endpoint * Performs an interrupt read of one 64 byte packet from the IN endpoint
* *
* @return buffer read from FCD * @return buffer read from FCD
* @throws LibUsbException on error * @throws FCDUsbException on error
*/ */
private ByteBuffer read() throws LibUsbException private ByteBuffer read() throws FCDUsbException
{ {
if(mDeviceHandle != null) UsbDeviceHandle handle = mDeviceHandle;
if(handle == null)
{ {
ByteBuffer buffer = ByteBuffer.allocateDirect(64); throw new FCDUsbException("device handle is null", LIBUSB_ERROR_NO_DEVICE);
IntBuffer transferred = IntBuffer.allocate(1);
int result = LibUsb.interruptTransfer(mDeviceHandle, FCD_ENDPOINT_IN, buffer, transferred, 500l);
if(result != LibUsb.SUCCESS)
{
throw new LibUsbException("error reading byte buffer", result);
}
else if(transferred.get(0) != buffer.capacity())
{
throw new LibUsbException("received bytes [" + transferred.get(0) + "] didn't match expected length [" +
buffer.capacity() + "]", result);
}
return buffer;
} }
throw new LibUsbException("device handle is null", LibUsb.ERROR_NO_DEVICE); byte[] packet = new byte[FCD_PACKET_SIZE];
int transferred;
try
{
transferred = handle.interruptTransfer(FCD_ENDPOINT_IN, packet, packet.length, FCD_TRANSFER_TIMEOUT_MS);
}
catch(UsbException e)
{
throw new FCDUsbException("error reading byte buffer", e.code());
}
if(transferred != packet.length)
{
throw new FCDUsbException("received bytes [" + transferred + "] didn't match expected length [" +
packet.length + "]", 0);
}
return ByteBuffer.wrap(packet);
} }
/** /**
@@ -590,9 +545,9 @@ public abstract class FCDTunerController extends TunerController
* *
* @param command - command to send * @param command - command to send
* @param argument - command argument to send * @param argument - command argument to send
* @throws LibUsbException - on error * @throws FCDUsbException - on error
*/ */
protected synchronized void send(FCDCommand command, long argument) throws LibUsbException protected synchronized void send(FCDCommand command, long argument) throws FCDUsbException
{ {
write(command, argument); write(command, argument);
read(); read();
@@ -603,9 +558,9 @@ public abstract class FCDTunerController extends TunerController
* command. * command.
* *
* @param command - command to send * @param command - command to send
* @throws LibUsbException - on error * @throws FCDUsbException - on error
*/ */
protected ByteBuffer send(FCDCommand command) throws LibUsbException protected ByteBuffer send(FCDCommand command) throws FCDUsbException
{ {
write(command); write(command);
return read(); return read();

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.fcd;
import io.github.dsheirer.libusb.LibUsb;
/**
* USB transfer error while communicating with a FUNcube Dongle. Unchecked, like the usb4java exception it replaces,
* since the FCD command methods are called from many places that handle any exception.
*/
public class FCDUsbException extends RuntimeException
{
private final int mErrorCode;
/**
* Constructs an instance
* @param message describing the failed operation
* @param errorCode libusb error code, or 0 when the transfer succeeded but returned unexpected data
*/
public FCDUsbException(String message, int errorCode)
{
super(errorCode < 0 ? message + " - " + LibUsb.errorName(errorCode) : message);
mErrorCode = errorCode;
}
/**
* libusb error code, such as -7 for LIBUSB_ERROR_TIMEOUT, or 0
*/
public int getErrorCode()
{
return mErrorCode;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ import javax.swing.JButton;
import javax.swing.JCheckBox; import javax.swing.JCheckBox;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator; import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel; import javax.swing.SpinnerNumberModel;
@@ -45,6 +46,7 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
private JButton mTunerInfoButton; private JButton mTunerInfoButton;
private JCheckBox mLnaGainCheckBox; private JCheckBox mLnaGainCheckBox;
private JCheckBox mMixerGainCheckBox; private JCheckBox mMixerGainCheckBox;
private JCheckBox mBiasTCheckBox;
/** /**
* Constructs an instance * Constructs an instance
@@ -106,11 +108,13 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
getTunerInfoButton().setEnabled(hasTuner()); getTunerInfoButton().setEnabled(hasTuner());
getLnaGainCheckBox().setEnabled(hasTuner()); getLnaGainCheckBox().setEnabled(hasTuner());
getMixerGainCheckBox().setEnabled(hasTuner()); getMixerGainCheckBox().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
if(hasTuner() && hasConfiguration()) if(hasTuner() && hasConfiguration())
{ {
getLnaGainCheckBox().setSelected(getConfiguration().getGainLNA()); getLnaGainCheckBox().setSelected(getConfiguration().getGainLNA());
getMixerGainCheckBox().setSelected(getConfiguration().getGainMixer()); getMixerGainCheckBox().setSelected(getConfiguration().getGainMixer());
getBiasTCheckBox().setSelected(getConfiguration().isBiasT());
} }
setLoading(false); setLoading(false);
@@ -135,8 +139,12 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
add(getFrequencyPanel(), "wrap"); add(getFrequencyPanel(), "wrap");
add(new JSeparator(), "span,growx,push"); add(new JSeparator(), "span,growx,push");
add(getLnaGainCheckBox()); JPanel checkBoxPanel = new JPanel();
add(getMixerGainCheckBox()); checkBoxPanel.setLayout(new MigLayout("insets 0", "[left][left][left]", ""));
checkBoxPanel.add(getLnaGainCheckBox());
checkBoxPanel.add(getMixerGainCheckBox());
checkBoxPanel.add(getBiasTCheckBox());
add(checkBoxPanel, "span,align left");
} }
public JButton getTunerInfoButton() public JButton getTunerInfoButton()
@@ -204,6 +212,37 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
return mMixerGainCheckBox; return mMixerGainCheckBox;
} }
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
public JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(event ->
{
if(hasTuner() && !isLoading())
{
try
{
getController().setBiasT(getBiasTCheckBox().isSelected());
save();
}
catch(SourceException e)
{
mLog.error("Couldn't set bias-T for FCD2", e);
JOptionPane.showMessageDialog(mBiasTCheckBox, "Couldn't set Bias-T: " + e.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
@Override @Override
public void setTunerLockState(boolean locked) public void setTunerLockState(boolean locked)
{ {
@@ -255,6 +294,7 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
getConfiguration().setGainLNA(getLnaGainCheckBox().isSelected()); getConfiguration().setGainLNA(getLnaGainCheckBox().isSelected());
getConfiguration().setGainMixer(getMixerGainCheckBox().isSelected()); getConfiguration().setGainMixer(getMixerGainCheckBox().isSelected());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
saveConfiguration(); saveConfiguration();
} }
} }

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