Compare commits

..

53 Commits

Author SHA1 Message Date
Denny Sheirer
09e19e1486 Merge pull request #1708 from tadscottsmith/rdio-scanner-filename
Explicitly send filename to Rdio-Scanner.
2023-11-07 04:12:14 -05:00
Denny Sheirer
0f5478925c Update README.md 2023-11-07 04:01:40 -05:00
Denny Sheirer
09dce0b0d1 Merge pull request #1710 from DSheirer/1709-nightly-build-timestamp-info
#1709 Nightly Release Build Timestamp Info
2023-11-07 03:32:42 -05:00
Dennis Sheirer
ad98b1cb5e #1709 Adds build timestamp info to nightly releases. 2023-11-07 03:31:31 -05:00
tadscottsmith
8effd97749 Explicitly send filename to Rdio-Scanner. 2023-11-06 10:54:41 -06:00
Denny Sheirer
9dbed72c32 Merge pull request #1707 from DSheirer/1706-nightly-release
#1706 Github Action For Nightly Releases
2023-11-06 05:06:19 -05:00
Dennis Sheirer
b4dd3692b8 #1706 Github action to create nightly application releases. 2023-11-06 05:05:40 -05:00
Denny Sheirer
030cbb856c Update README.md 2023-11-06 04:36:59 -05:00
Denny Sheirer
511101e9b7 Merge pull request #1705 from DSheirer/1704-harris-patch-group-formatting
#1704 Patch Group Formatting
2023-11-06 02:52:49 -05:00
Dennis Sheirer
60dd2b2df7 #1704 Updates patch group format for compatibility. 2023-11-06 02:50:46 -05:00
Denny Sheirer
218dc94e02 Merge pull request #1701 from DSheirer/1644-sdrplay-api-3-12
#1644 SDRPlay API Version 3.12 For Mac OSX
2023-11-05 07:22:04 -05:00
Dennis Sheirer
2440f578b1 #1644 Update SDRPlay API for version 3.12 compatability 2023-11-05 07:20:29 -05:00
Denny Sheirer
09c87237cb Merge pull request #1700 from DSheirer/1651-gradle-build-version-naming
#1651 Version 0.6.0 Beta 4 and Version Naming Conventions
2023-11-05 07:10:00 -05:00
Dennis Sheirer
3d974ddd91 #1651 Increment version to 0.6.0 Beta 4 and add version naming conventions comments. 2023-11-05 07:08:18 -05:00
Denny Sheirer
1489c970ba Merge pull request #1699 from DSheirer/1653-channel-edit-with-filtering
#1653 Playlist Channel Edits With Applied Filter Produces Error
2023-11-05 06:58:36 -05:00
Dennis Sheirer
9e0afca3d1 #1653 Playlist channel edits with applied filter doesn't produce error 2023-11-05 06:56:49 -05:00
Denny Sheirer
99eda4013e Merge pull request #1698 from DSheirer/1654-playlist-editor-channel-frequency-sort
#1654 Playlist Channel Editor Frequency Column Sort
2023-11-05 06:32:23 -05:00
Dennis Sheirer
a62f81f890 #1654 Playlist channel editor sorts by frequency numerically (vs lexicographically) 2023-11-05 06:29:23 -05:00
Denny Sheirer
66f0731731 Merge pull request #1697 from DSheirer/1665-refresh-broadcastify-streams
#1665 Refresh Broadcastify Streams In Playlist Editor
2023-11-05 06:00:48 -05:00
Dennis Sheirer
dc239801c6 #1665 Add option to refresh available streams from broadcastify in playlist editor's streaming tab. 2023-11-05 05:58:32 -05:00
Denny Sheirer
d4ba0988bc Merge pull request #1696 from DSheirer/1673-playlist-editor-broadcastify-system-id
#1673 Broadcastify Call Streaming Config Editor's default value of 0 …
2023-11-05 05:24:03 -05:00
Dennis Sheirer
ad0f953b2a #1673 Broadcastify Call Streaming Config Editor's default value of 0 causes field to be uneditable. 2023-11-05 05:20:15 -05:00
Denny Sheirer
e1bc9eabd6 Merge pull request #1695 from DSheirer/1694-sdrplay-rsp-tuner-memory-leaks
#1694 SDRPlay RSP Memory Leak & Audio Playback Delays
2023-11-05 04:26:48 -05:00
Dennis Sheirer
d3dc1a8667 #1694 Resolves memory leak in SDRPlay RSP tuner implementation. Sample stream and event callback functions were using a global arena for memory allocation where memory was not being de-allocated. Changed to on-demand arena creation and deterministic memory de-allocation. Also, when using a reduced sample rate from the RSP tuner, the dispatcher was not keeping up with the inbound sample buffer rate, resulting in a buildup of RSP native sample buffers over time and causing growing audio delays with broken/stilted audio during playback. 2023-11-05 04:24:19 -05:00
Denny Sheirer
bf278120de Merge pull request #1692 from DSheirer/DSheirer-patch-1
Update README.md
2023-11-04 07:41:29 -04:00
Denny Sheirer
2f52f6e009 Update README.md 2023-11-04 07:41:12 -04:00
Denny Sheirer
02363c3113 Merge pull request #1690 from DSheirer/1689-harris-patch-group-prefixes
#1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris Patch Groups
2023-11-04 07:13:10 -04:00
Dennis Sheirer
cfaac0fcbc #1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris patch groups. 2023-11-04 07:12:29 -04:00
Denny Sheirer
bee0405e00 Merge pull request #1688 from DSheirer/1687-rtl2832-fc0013
Support For RTL2832 With Embedded FC0013 Tuner
2023-11-04 06:54:55 -04:00
Dennis Sheirer
50bdf0ee81 #1687 Adds support for RTL2832 with embedded FC0013 tuner. 2023-11-04 06:52:37 -04:00
Denny Sheirer
a27ddbe0d0 Merge pull request #1679 from DSheirer/1643-r828d-v4-tuner
#1643 RTL2832/R828D aka RTL-SDR V4 Dongle & Bias-T Support
2023-10-22 08:40:33 -04:00
Dennis Sheirer
0d652b9153 #1643 Adds support for RTL2832 with embedded R828D tuner and RTL-SDR.com V4 dongles. Introduces Bias-T support for all RTL-2832 dongles. 2023-10-22 08:38:16 -04:00
Denny Sheirer
9468fd189b Merge pull request #1677 from DSheirer/1607-harris-p25-patch-groups
#1607 P25 P1/P2 L3Harris Patch Group Support
2023-10-21 07:00:41 -04:00
sheirerd
215737ad9b #1607 Add support for L3Harris dynamic regroup/patch messages and events. Includes parsing for L3Harris unknown opcodes 10, 42 and 43. 2023-10-21 06:58:11 -04:00
Denny Sheirer
123c25b142 Merge pull request #1672 from DSheirer/1650-fm-decoder-7_5-bandwidth
#1650 Adds 7.5 kHz channel bandwidth option for FM channels.
2023-10-15 09:22:48 -04:00
Dennis Sheirer
af56198f0e #1650 Adds 7.5 kHz channel bandwidth option for FM channels. 2023-10-15 09:21:38 -04:00
Denny Sheirer
3c73fdfd6e Merge pull request #1671 from DSheirer/1670-dmr-cap-plus-missing-call-audio
#1670 Missing Audio Segments - DMR Cap+
2023-10-15 08:56:48 -04:00
Dennis Sheirer
b391ebbb5f #1670 Resolves missing audio segments for some DMR Cap+ call events. 2023-10-15 08:55:35 -04:00
Denny Sheirer
bad5a1f9f0 Merge pull request #1669 from DSheirer/1668-message-viewer-p25-support
#1668 Message Recording Viewer - P25P1 Support
2023-10-15 07:17:06 -04:00
Dennis Sheirer
a7470b463f #1668 Updates message recording (.bits) viewer to support P25 Phase 1. 2023-10-15 07:14:53 -04:00
Denny Sheirer
d6c706cb35 Merge pull request #1667 from DSheirer/1666-p25-moto-unit-gps
#1666 P25 Motorola Unit GPS Parsing & Plot to Map
2023-10-14 08:16:48 -04:00
Dennis Sheirer
a0d6950c75 #1666 P25 Motorola unit self-reported GPS location parsing and plotting to map. 2023-10-14 08:15:35 -04:00
Denny Sheirer
2495200b36 Merge pull request #1663 from DSheirer/1648-dmr-lrrp-not-plotting-map
#1648 DMR Embedded GPS & LRRP Plotting
2023-10-05 03:32:08 -04:00
Dennis Sheirer
bfe7ae0b1b #1648 Resolves issue with plotting DMR embedded GPS and LRRP positions. 2023-10-05 03:31:17 -04:00
Denny Sheirer
4912abbf07 Merge pull request #1662 from DSheirer/1661-dmr-gps-location-parsing-error
#1661 DMR Talker GPS Lat/Long Values Incorrect
2023-10-05 03:21:24 -04:00
Dennis Sheirer
2f2998d02e #1661 Resolves issue with parsing of DMR embedded GPS latitude and longitude values. 2023-10-05 03:20:17 -04:00
Dennis Sheirer
c96349fa1c Updates version to 0.6.0 Beta 2 2023-08-31 07:14:58 -04:00
Denny Sheirer
f640576cc3 Merge pull request #1646 from DSheirer/1637-dmr-cap-plus-and-max-trunking
#1637 DMR Cap+/CapMax Channel Frequency Map / Trunking Issue
2023-08-31 07:10:21 -04:00
Dennis Sheirer
77d725a488 #1637 Resolves issue with DMR Cap+/CapMax trunking channel to frequency map. Updates the DMR channel map to use LCN instead of LSN and aligns standard DMR with Tier-III DMR for channel identification.
Warning: breaking change.  Users will need to either recreate their DMR channel to frequency maps or simply delete and reload the channel from Radio Reference to correct the channel to frequency map.

Updates the DMR Decoder State to better track and update DMR events versus creating a new event and causing lots of scrolling in the event window of the Now Playing tab.
2023-08-31 07:06:50 -04:00
Denny Sheirer
2c0305f928 Merge pull request #1642 from DSheirer/1641-hytera-rrs-packets
#1641 DMR Hytera RRS packet support.
2023-08-27 07:47:14 -04:00
Dennis Sheirer
21fd61d507 #1641 DMR Hytera RRS packet support. 2023-08-27 07:44:43 -04:00
Denny Sheirer
8fb56a5342 Merge pull request #1640 from DSheirer/1638-hytera-encryption-parameters
#1638 DMR Decoder - Encryption Parameters & Hytera RRS
2023-08-26 07:36:31 -04:00
Dennis Sheirer
5d7b73f876 #1638 Enhances DMR decoder to include additional encryption details from two major vendor implementations. Adds Hytera short data Radio Registration Service (RRS) processing. 2023-08-26 07:32:46 -04:00
161 changed files with 6183 additions and 1817 deletions

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:

47
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# Create nightly release for Linux, Mac and Windows using the Linux runner.
name: Nightly Release
on:
push:
branches: [ master ]
# Don't run nightly build for pull requests - uncomment to test changes to this action within scope of the PR review
# pull_request:
# branches: [ master ]
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:
java-version: '20'
distribution: 'liberica'
java-package: 'jdk+fx'
- name: Build Linux and Mac releases with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: runtimeZipOthers -PprojectVersion=nightly
- name: Build Windows release with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: runtimeZipWindows -PprojectVersion=nightly
- name: Create Build Info file
run: |
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
- name: Add build artifacts to nightly release
uses: pyTooling/Actions/releaser@main
with:
tag: nightly
rm: false
token: ${{ secrets.GITHUB_TOKEN }}
files: |
build/image/*.zip
build/image/build_info.txt

View File

@@ -1,8 +1,10 @@
![Gradle Build](https://github.com/dsheirer/sdrtrunk/actions/workflows/gradle.yml/badge.svg)
![Nightly Release](https://github.com/dsheirer/sdrtrunk/actions/workflows/nightly.yml/badge.svg)
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
* [Help/Wiki Home Page](https://github.com/DSheirer/sdrtrunk/wiki)
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
@@ -18,6 +20,13 @@ All release versions of sdrtrunk are available from the [releases](https://githu
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
* **(final)** These versions have been tested and are the current release version.
## Download Nightly Software Build
The [nightly](https://github.com/DSheirer/sdrtrunk/releases/tag/nightly) release contains current builds of the software
for all supported operating systems. This version of the software may contain bugs and may not run correctly. However,
it let's you preview the most recent changes and fixes before the next software release. **Always backup your
playlist(s) before you use the nightly builds.** Note: the nightly release is updated each time code changes are
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
## Minimum System Requirements
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **CPU:** 4-core

View File

@@ -53,7 +53,8 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.6.0-beta1'
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 20 is required for this version of the Project Panama preview/incubator feature
java {

29
gradle.properties Normal file
View File

@@ -0,0 +1,29 @@
#
# ******************************************************************************
# Copyright (C) 2014-2023 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
# *****************************************************************************
#
# Version naming conventions:
# Alpha: maj.min.patch-alpha-build
# Beta: maj.min.patch-beta-build
# Release: maj.min.patch
#
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.6.0-beta-4

View File

@@ -388,6 +388,8 @@ public class AliasList
}
break;
case PATCH_GROUP:
List<Alias> aliases = new ArrayList<>();
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
PatchGroup patchGroup = patchGroupIdentifier.getValue();
@@ -395,8 +397,6 @@ public class AliasList
if(patchGroupAliasList != null)
{
List<Alias> aliases = new ArrayList<>();
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
if(alias != null)
@@ -404,19 +404,36 @@ public class AliasList
aliases.add(alias);
}
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers())
for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
{
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup);
Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
if(patchedAlias != null && !aliases.contains(patchedAlias))
if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
{
aliases.add(patchedAlias);
aliases.add(patchedTalkgroupAlias);
}
}
return aliases;
}
break;
if(patchGroup.hasPatchedRadios())
{
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
if(radioAliasList != null)
{
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
{
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
{
aliases.add(patchedRadioAlias);
}
}
}
}
return aliases;
case RADIO:
RadioIdentifier radio = (RadioIdentifier)identifier;
@@ -483,9 +500,7 @@ public class AliasList
{
if(alias != null)
{
List<Alias> aliases = new ArrayList<>();
aliases.add(alias);
return aliases;
return Collections.singletonList(alias);
}
return Collections.emptyList();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,9 +38,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
StringBuilder sb = new StringBuilder();
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(",").append(patched.getValue());
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
@@ -34,7 +33,6 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
@@ -42,24 +40,12 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
@@ -244,6 +232,9 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
String talkgroupLabel = getTalkgroupLabel(audioRecording);
String talkgroupGroup = getTalkgroupGroup(audioRecording);
String systemLabel = getSystemLabel(audioRecording);
String path = audioRecording.getPath().toString();
// Remove TEMPORARY_STREAM_FILE_SUFFIX
String audioName = path.substring(path.substring(0, path.lastIndexOf("_")).lastIndexOf("_") + 1);
try
{
@@ -264,6 +255,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
.addAudioName(audioName)
.addFile(audioBytes)
.addPart(FormField.DATE_TIME, timestampSeconds)
.addPart(FormField.TALKGROUP_ID, talkgroup)
@@ -525,7 +517,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(",").append(patched.getValue());
}

View File

@@ -34,7 +34,8 @@ public class RdioScannerBuilder
private static final String DASH_DASH = "--";
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
private List<Part> mParts = new ArrayList<>();
private byte[] audioBytes = null;
private byte[] audioBytes = null;
private String audioName = null;
/**
* Constructs an instance
@@ -51,6 +52,15 @@ public class RdioScannerBuilder
return BOUNDARY;
}
/**
* Adds a Audio file name part to the call
*/
public RdioScannerBuilder addAudioName(String value)
{
audioName = value;
return this;
}
/**
* Adds a Audio bytes part to the call
*/
@@ -110,7 +120,8 @@ public class RdioScannerBuilder
}
StringBuilder sb= new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
sb.append("Content-Disposition: form-data; filename =\"").append(audioName);
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
return sb.toString();
}

View File

@@ -844,13 +844,18 @@ public class BinaryMessage extends BitSet
*/
public int getTwosComplement(int start, int end)
{
BinaryMessage sub = getSubMessage(start, end);
boolean negative = sub.get(start);
sub.flip(0, sub.size());
int value = sub.getInt(0, sub.size()) + 1;
value *= (negative ? -1 : 1);
return value;
if(get(start))
{
//Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
fragment.flip(0, fragment.size());
return fragment.getInt(1, fragment.size()) + 1;
}
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
}
}

View File

@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
//Sized to process 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{
public IFFTProcessorDispatcher(int batchSize, long interval)
public IFFTProcessorDispatcher(long interval)
{
super("sdrtrunk polyphase ifft processor", batchSize, interval);
super("sdrtrunk polyphase ifft processor", interval);
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
@@ -65,6 +66,7 @@ import org.slf4j.LoggerFactory;
public class PolyphaseChannelManager implements ISourceEventProcessor
{
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
private static final double CHANNEL_OVERSAMPLING = 2.0;
@@ -108,7 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 50, 10);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
mBufferDispatcher.setListener(mNativeBufferReceiver);
}
@@ -203,7 +205,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
catch(IllegalArgumentException iae)
{
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
channelSource = null;
}
}

View File

@@ -50,8 +50,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mInputChannelCount = inputChannelCount;
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
mHeartbeatManager = heartbeatManager;
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
50, mHeartbeatManager);
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> {
try
{

View File

@@ -24,8 +24,6 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelRange;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.DMRRecordingViewer;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.IconManager;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.PlaylistEditor;
@@ -37,6 +35,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
@@ -67,7 +67,7 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER = "dmr.message.viewer";
public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
@@ -82,14 +82,14 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager;
private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor;
private DMRRecordingViewer mDmrRecordingViewer;
private RecordingViewer mRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
private Stage mDmrRecordingViewerStage;
private Stage mRecordingViewerStage;
/**
* Constructs an instance. Note: this constructor is used for Swing applications.
@@ -195,31 +195,31 @@ public class JavaFxWindowManager extends Application
}
/**
* Stage for the DMR Message Viewer
* Stage for the recording viewer
*/
public Stage getDmrRecordingViewerStage()
public Stage getRecordingViewerStage()
{
if(mDmrRecordingViewerStage == null)
if(mRecordingViewerStage == null)
{
createJFXPanel();
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800);
mDmrRecordingViewerStage = new Stage();
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer");
mDmrRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER);
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
mRecordingViewerStage = new Stage();
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
mRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
}
return mDmrRecordingViewerStage;
return mRecordingViewerStage;
}
public DMRRecordingViewer getDmrRecordingViewer()
public RecordingViewer getRecordingViewer()
{
if(mDmrRecordingViewer == null)
if(mRecordingViewer == null)
{
mDmrRecordingViewer = new DMRRecordingViewer();
mRecordingViewer = new RecordingViewer();
}
return mDmrRecordingViewer;
return mRecordingViewer;
}
public Stage getIconManagerStage()
@@ -453,12 +453,12 @@ public class JavaFxWindowManager extends Application
* Process a channel map editor request
*/
@Subscribe
public void process(final ViewDmrRecordingViewerRequest request)
public void process(final ViewRecordingViewerRequest request)
{
execute(() -> {
getDmrRecordingViewerStage().show();
getDmrRecordingViewerStage().requestFocus();
getDmrRecordingViewerStage().toFront();
getRecordingViewerStage().show();
getRecordingViewerStage().requestFocus();
getRecordingViewerStage().toFront();
});
}

View File

@@ -32,13 +32,13 @@ import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService;
@@ -420,9 +420,9 @@ public class SDRTrunk implements Listener<TunerEvent>
viewMenu.add(new JSeparator());
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
viewMenu.add(dmrMessageViewerMenu);
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(recordingViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));

View File

@@ -1,33 +1,29 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* 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.gui.control;
import java.util.function.UnaryOperator;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.converter.IntegerStringConverter;
import java.util.function.UnaryOperator;
/**
* Text field control for integer values.
*/
@@ -52,7 +48,7 @@ public class IntegerTextField extends TextField
return null;
};
setTextFormatter(new TextFormatter<Integer>(new IntegerStringConverter(), 0, filter));
setTextFormatter(new TextFormatter<Integer>(new IntegerStringConverter(), null, filter));
}
/**
@@ -71,7 +67,7 @@ public class IntegerTextField extends TextField
//Do nothing ... we couldn't parse the value
}
return null;
return 0;
}
/**

View File

@@ -92,9 +92,9 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public AMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,8 @@ import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -69,9 +71,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.function.Predicate;
/**
* Channel configuration editor
*/
@@ -100,6 +99,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private IconNode mPlayGraphicNode;
private IconNode mStopGraphicNode;
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
private IFilterProcessor mFilterProcessor;
/**
* Constructs an instance
@@ -108,11 +108,12 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
* @param userPreferences for preferences
*/
public ChannelConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
mPlaylistManager = playlistManager;
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mFilterProcessor = filterProcessor;
setMaxWidth(Double.MAX_VALUE);
@@ -689,7 +690,16 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
mSaveButton.setMaxWidth(Double.MAX_VALUE);
mSaveButton.disableProperty().bind(modifiedProperty().not());
mSaveButton.setOnAction(event -> {
save();
if(mFilterProcessor != null)
{
mFilterProcessor.clearFilter();
save();
mFilterProcessor.restoreFilter();
}
else
{
save();
}
if(getItem().isProcessing())
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,11 +23,10 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides access to channel configuration editors for various decoder types.
@@ -41,31 +40,35 @@ public class ChannelConfigurationEditorFactory
* Constructs an editor for the specified decoder type
* @param decoderType to create
* @param playlistManager for the editor
* @param tunerManager for tuners
* @param userPreferences for preferences
* @param filterProcessor to be notified to clear and restore any applied filters.
* @return constructed editor
*/
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
TunerManager tunerManager, UserPreferences userPreferences)
TunerManager tunerManager, UserPreferences userPreferences,
IFilterProcessor filterProcessor)
{
switch(decoderType)
{
case AM:
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case DMR:
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case NBFM:
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR_NET:
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR:
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case MPT1327:
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case PASSPORT:
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case P25_PHASE1:
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case P25_PHASE2:
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
default:
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +26,12 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@@ -63,17 +69,10 @@ import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
/**
* JavaFX editor for managing channel configurations.
*/
public class ChannelEditor extends SplitPane
public class ChannelEditor extends SplitPane implements IFilterProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
private PlaylistManager mPlaylistManager;
@@ -106,7 +105,8 @@ public class ChannelEditor extends SplitPane
mPlaylistManager = playlistManager;
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager, userPreferences);
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
userPreferences, this);
HBox channelsBox = new HBox();
channelsBox.setSpacing(10.0);
@@ -203,7 +203,7 @@ public class ChannelEditor extends SplitPane
if(editor == null)
{
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
mTunerManager, mUserPreferences);
mTunerManager, mUserPreferences, this);
if(editor != null)
{
@@ -374,6 +374,24 @@ public class ChannelEditor extends SplitPane
mChannelFilteredList.setPredicate(mChannelListFilter);
}
/**
* Temporarily clear the filter when the channel configuration editor is applying changes.
*/
@Override
public void clearFilter()
{
mChannelFilteredList.setPredicate(null);
}
/**
* Restore the filter once the channel configuration editor has completed applying changes.
*/
@Override
public void restoreFilter()
{
mChannelFilteredList.setPredicate(mChannelListFilter);
}
private TableView<Channel> getChannelTableView()
{
if(mChannelTableView == null)
@@ -449,6 +467,25 @@ public class ChannelEditor extends SplitPane
nameColumn.setPrefWidth(200);
TableColumn frequencyColumn = new TableColumn("Frequency");
frequencyColumn.setComparator((o1, o2) -> {
if(o1 instanceof Channel c1 && o2 instanceof Channel c2)
{
if(!c1.getFrequencyList().isEmpty() && !c2.getFrequencyList().isEmpty())
{
return Long.compare(c1.getFrequencyList().get(0), c2.getFrequencyList().get(0));
}
else if(!c1.getFrequencyList().isEmpty())
{
return Long.compare(c1.getFrequencyList().get(0), 0l);
}
else if(!c2.getFrequencyList().isEmpty())
{
return Long.compare(0l, c2.getFrequencyList().get(0));
}
}
return Integer.compare(o1.hashCode(), o2.hashCode());
});
frequencyColumn.setCellValueFactory(new FrequencyCellValueFactory());
frequencyColumn.setPrefWidth(100);

View File

@@ -85,7 +85,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
private ToggleSwitch mUseCompressedTalkgroupsToggle;
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
private IntegerTextField mLogicalSlotNumberField;
private IntegerTextField mLogicalChannelNumberField;
private FrequencyField mDownlinkFrequencyField;
// private FrequencyField mUplinkFrequencyField;
private Button mAddTimeslotFrequencyButton;
@@ -99,9 +99,9 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public DMRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());
@@ -172,7 +172,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row);
gridPane.getChildren().add(useCompressedTalkgroupsLabel);
Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies");
Label timeslotTableLabel = new Label("Logical Channel Number (LCN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies. LSN = Logical Slot Number");
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
gridPane.getChildren().add(timeslotTableLabel);
@@ -194,17 +194,13 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
editorBox.setAlignment(Pos.CENTER_LEFT);
editorBox.setSpacing(5);
Label lsnLabel = new Label("LSN");
editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField());
Label lcnLabel = new Label("LCN");
editorBox.getChildren().addAll(lcnLabel, getLogicalChannelNumberField());
Label downlinkLabel = new Label("Frequency (MHz)");
downlinkLabel.setPadding(new Insets(0,0,0,5));
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
// Label uplinkLabel = new Label("Uplink");
// uplinkLabel.setPadding(new Insets(0,0,0,5));
// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField());
GridPane.setConstraints(editorBox, 0, row, 4, 1);
gridPane.getChildren().add(editorBox);
@@ -287,20 +283,22 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
mTimeslotFrequencyTable.setPrefHeight(100.0);
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LSN");
numberColumn.setPrefWidth(70);
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LCN");
numberColumn.setPrefWidth(75);
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
downlinkColumn.setPrefWidth(175);
downlinkColumn.setPrefWidth(150);
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
// TableColumn<TimeslotFrequency,Number> uplinkColumn = new TableColumn("Uplink (MHz)");
// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz());
// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn);
TableColumn<TimeslotFrequency,String> lsnColumn = new TableColumn("IDs (TS1/TS2)");
lsnColumn.setPrefWidth(225);
lsnColumn.setCellValueFactory(new PropertyValueFactory<>("logicalSlotNumbers"));
mTimeslotFrequencyTable.getColumns().addAll(lsnColumn);
mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
@@ -317,20 +315,20 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
//Preserve the current modified flag state since setting values in the editor will change it.
boolean modified = modifiedProperty().get();
getLogicalSlotNumberField().setDisable(timeslot == null);
getLogicalChannelNumberField().setDisable(timeslot == null);
getDownlinkFrequencyField().setDisable(timeslot == null);
// getUplinkFrequencyField().setDisable(timeslot == null);
getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
if(timeslot != null)
{
getLogicalSlotNumberField().set(timeslot.getNumber());
getLogicalChannelNumberField().set(timeslot.getNumber());
getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
// getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
}
else
{
getLogicalSlotNumberField().set(0);
getLogicalChannelNumberField().set(0);
getDownlinkFrequencyField().set(0);
// getUplinkFrequencyField().set(0);
}
@@ -412,19 +410,19 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
return mDeleteTimeslotFrequencyButton;
}
private IntegerTextField getLogicalSlotNumberField()
private IntegerTextField getLogicalChannelNumberField()
{
if(mLogicalSlotNumberField == null)
if(mLogicalChannelNumberField == null)
{
mLogicalSlotNumberField = new IntegerTextField();
mLogicalSlotNumberField.setDisable(true);
mLogicalSlotNumberField.setPrefWidth(65);
mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
mLogicalChannelNumberField = new IntegerTextField();
mLogicalChannelNumberField.setDisable(true);
mLogicalChannelNumberField.setPrefWidth(65);
mLogicalChannelNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
if(selected != null)
{
Integer value = mLogicalSlotNumberField.get();
Integer value = mLogicalChannelNumberField.get();
if(value != null)
{
@@ -436,7 +434,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
});
}
return mLogicalSlotNumberField;
return mLogicalChannelNumberField;
}
private FrequencyField getDownlinkFrequencyField()
@@ -610,8 +608,8 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
getTimeslotTable().setDisable(config == null);
getAddTimeslotFrequencyButton().setDisable(config == null);
getDeleteTimeslotFrequencyButton().setDisable(true);
getLogicalSlotNumberField().set(0);
getLogicalSlotNumberField().setDisable(true);
getLogicalChannelNumberField().set(0);
getLogicalChannelNumberField().setDisable(true);
getDownlinkFrequencyField().set(0);
getDownlinkFrequencyField().setDisable(true);
// getUplinkFrequencyField().set(0);

View File

@@ -0,0 +1,30 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.channel;
/**
* Interface for a processor that implements a filtering mechanism and can be commanded by an external controller to
* temporarily clear a filter and then restore the filter.
*/
public interface IFilterProcessor
{
void clearFilter();
void restoreFilter();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* LTR (standard) channel configuration editor
*/
@@ -77,9 +76,9 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public LTRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* LTR-Net channel configuration editor
*/
@@ -76,9 +75,10 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
* @param tunerManager for tuners
* @param userPreferences for preferences
*/
public LTRNetConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
public LTRNetConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
@@ -56,9 +58,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MAXIMUM;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MINIMUM;
@@ -87,9 +86,9 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -27,7 +27,6 @@ import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
@@ -41,6 +40,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
@@ -58,9 +59,6 @@ import javafx.scene.text.TextAlignment;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.ToggleSwitch;
import java.util.ArrayList;
import java.util.List;
/**
* Narrow-Band FM channel configuration editor
*/
@@ -95,9 +93,9 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public NBFMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -54,9 +56,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* P25 Phase 1 channel configuration editor
*/
@@ -83,9 +82,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public P25P1ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -46,9 +48,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* P25 Phase 2 channel configuration editor
*/
@@ -72,9 +71,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences
*/
public P25P2ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
@@ -43,9 +45,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Passport channel configuration editor
*/
@@ -68,9 +67,9 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences
*/
public PassportConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -45,9 +45,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
* @param aliasModel
*/
public UnknownConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getDecoderPane());
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -41,13 +41,12 @@ import io.github.dsheirer.rrapi.type.Tag;
import io.github.dsheirer.rrapi.type.Talkgroup;
import io.github.dsheirer.rrapi.type.Type;
import io.github.dsheirer.rrapi.type.Voice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences.
@@ -374,16 +373,10 @@ public class RadioReferenceDecoder
}
}
int lsn = (lcn - 1) * 2 + 1;
TimeslotFrequency timeslot1Frequency = new TimeslotFrequency();
timeslot1Frequency.setNumber(lsn);
timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
frequencies.add(timeslot1Frequency);
TimeslotFrequency timeslot2Frequency = new TimeslotFrequency();
timeslot2Frequency.setNumber(lsn + 1);
timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
frequencies.add(timeslot2Frequency);
TimeslotFrequency timeslotFrequency = new TimeslotFrequency();
timeslotFrequency.setNumber(lcn);
timeslotFrequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
frequencies.add(timeslotFrequency);
}
return frequencies;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,11 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -48,6 +53,7 @@ import javafx.scene.control.TabPane;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -58,12 +64,6 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Editor for broadcast audio stream configurations
*/
@@ -75,6 +75,7 @@ public class StreamingEditor extends SplitPane
private TableView<ConfiguredBroadcast> mConfiguredBroadcastTableView;
private MenuButton mNewButton;
private Button mDeleteButton;
private Button mRefreshButton;
private TabPane mTabPane;
private Tab mConfigurationTab;
private Tab mAliasTab;
@@ -101,7 +102,7 @@ public class StreamingEditor extends SplitPane
refreshBroadcastifyStreams();
VBox buttonsBox = new VBox();
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton());
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton(), getRefreshButton());
buttonsBox.setPadding(new Insets(0, 0, 0, 10));
buttonsBox.setSpacing(10);
@@ -231,21 +232,16 @@ public class StreamingEditor extends SplitPane
{
if(mPlaylistManager.getRadioReference().availableProperty().get())
{
ThreadPool.CACHED.submit(new Runnable()
{
@Override
public void run()
ThreadPool.CACHED.submit(() -> {
try
{
try
{
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
mBroadcastifyFeeds.clear();
mBroadcastifyFeeds.addAll(feeds);
}
catch(Throwable t)
{
mLog.error("Unable to refresh broadcastify stream configuration(s)");
}
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
mBroadcastifyFeeds.clear();
mBroadcastifyFeeds.addAll(feeds);
}
catch(Throwable t)
{
mLog.error("Unable to refresh broadcastify stream configuration(s)");
}
});
}
@@ -348,6 +344,22 @@ public class StreamingEditor extends SplitPane
return mNewButton;
}
/**
* Refresh broadcastify feeds.
* @return button to refresh.
*/
private Button getRefreshButton()
{
if(mRefreshButton == null)
{
mRefreshButton = new Button("Refresh");
mRefreshButton.setTooltip(new Tooltip("Refresh streams available from Broadcastify"));
mRefreshButton.setOnAction(event -> refreshBroadcastifyStreams());
}
return mRefreshButton;
}
private Button getDeleteButton()
{
if(mDeleteButton == null)

View File

@@ -17,20 +17,22 @@
* ****************************************************************************
*/
package io.github.dsheirer.gui.dmr;
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@@ -40,10 +42,10 @@ import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
@@ -58,21 +60,19 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility application to load and view a DMR .bits recording file with the messages fully parsed.
* DMR Viewer panel
*/
public class DMRRecordingViewer extends VBox
public class DmrViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(DMRRecordingViewer.class);
private static final Logger mLog = LoggerFactory.getLogger(DmrViewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory";
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class);
private Scene mScene;
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr";
private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
@@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public DMRRecordingViewer()
public DmrViewer()
{
setPadding(new Insets(5));
setSpacing(5);
@@ -132,6 +133,21 @@ public class DMRRecordingViewer extends VBox
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/**
* Processes the recording file and loads the content into the viewer
* @param file containing a .bits recording of decoded DMR data.
@@ -140,33 +156,45 @@ public class DMRRecordingViewer extends VBox
{
if(file != null && file.exists())
{
getSelectedFileLabel().setText(file.getName());
mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
final boolean useCompressed = getUseCompressedTalkgroups().isSelected();
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
DecodeConfigDMR config = new DecodeConfigDMR();
if(getUseCompressedTalkgroups().isSelected())
ThreadPool.CACHED.submit(new Runnable()
{
config.setUseCompressedTalkgroups(true);
}
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> mMessages.add(message));
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
@Override
public void run()
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
List<IMessage> messages = new ArrayList<>();
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
DecodeConfigDMR config = new DecodeConfigDMR();
config.setUseCompressedTalkgroups(useCompressed);
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> messages.add(message));
getMessageTableView().scrollTo(0);
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
}
});
}
}
@@ -176,9 +204,9 @@ public class DMRRecordingViewer extends VBox
private void updateFilters()
{
Predicate<IMessage> timeslotPredicate = message ->
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
String filterText = getSearchText().getText();
@@ -256,6 +284,7 @@ public class DMRRecordingViewer extends VBox
if(mMessageTableView == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
@@ -509,37 +538,9 @@ public class DMRRecordingViewer extends VBox
{
if(mUseCompressedTalkgroups == null)
{
mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups");
mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups");
}
return mUseCompressedTalkgroups;
}
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new DMRRecordingViewer(), 1100, 800);
primaryStage.setTitle("DMR Recording Viewer");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting DMR recording viewer application", e);
}
};
Platform.startup(r);
}
}

View File

@@ -0,0 +1,462 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* APCO25 Phase 1 viewer panel
*/
public class P25P1Viewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p1";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public P25P1Viewer()
{
setPadding(new Insets(5));
setSpacing(5);
HBox fileBox = new HBox();
fileBox.setMaxWidth(Double.MAX_VALUE);
fileBox.setAlignment(Pos.CENTER_LEFT);
fileBox.setSpacing(5);
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
HBox filterBox = new HBox();
filterBox.setMaxWidth(Double.MAX_VALUE);
filterBox.setAlignment(Pos.BASELINE_CENTER);
filterBox.setSpacing(5);
Label searchLabel = new Label("Message Filter:");
HBox.setMargin(searchLabel, new Insets(0,0,0,15));
Label findLabel = new Label("Find:");
HBox.setHgrow(getFindText(), Priority.ALWAYS);
HBox.setHgrow(getSearchText(), Priority.ALWAYS);
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
getSearchText());
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
* Processes the recording file and loads the content into the viewer
*
* Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
* message table back on the JavaFX UI thread.
*
* @param file containing a .bits recording of decoded DMR data.
*/
private void load(File file)
{
if(file != null && file.exists())
{
mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
List<IMessage> messages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
messages.add(message);
}
});
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
});
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
String filterText = getSearchText().getText();
if(filterText != null && !filterText.isEmpty())
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(textPredicate);
}
else
{
mFilteredMessages.setPredicate(null);
}
}
/**
* Finds and selects the first row containing the text argument.
* @param text to search for.
*/
private void find(String text)
{
if(text != null && !text.isEmpty())
{
for(IMessage message: mFilteredMessages)
{
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
/**
* Finds and selects the first row containing the text argument, after the currently selected row.
* @param text to search for.
*/
private void findNext(String text)
{
if(text != null && !text.isEmpty())
{
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
find(text);
return;
}
int row = mFilteredMessages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessages.size(); x++)
{
if(x < mFilteredMessages.size())
{
IMessage message = mFilteredMessages.get(x);
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
}
/**
* List view control with DMR messages
*/
private TableView<IMessage> getMessageTableView()
{
if(mMessageTableView == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
mMessageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
final StringBuilder sb = new StringBuilder();
boolean firstRow = true;
for (final Integer row : rows)
{
if(firstRow)
{
firstRow = false;
}
else
{
sb.append('\n');
}
boolean firstCol = true;
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
{
if(firstCol)
{
firstCol = false;
}
else
{
sb.append('\t');
}
final Object cellData = column.getCellData(row);
sb.append(cellData == null ? "" : cellData.toString());
}
}
final ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(sb.toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
});
TableColumn timestampColumn = new TableColumn();
timestampColumn.setPrefWidth(110);
timestampColumn.setText("Time");
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
TableColumn validColumn = new TableColumn();
validColumn.setPrefWidth(50);
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
TableColumn messageColumn = new TableColumn();
messageColumn.setPrefWidth(1000);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof IMessage message)
{
property.set(message.toString());
}
return property;
});
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
}
return mMessageTableView;
}
/**
* File selection button
* @return button
*/
private Button getSelectFileButton()
{
if(mSelectFileButton == null)
{
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select DMR .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{
File file = new File(lastDirectory);
if(file.exists() && file.isDirectory())
{
fileChooser.setInitialDirectory(file);
}
}
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
{
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}
});
}
return mSelectFileButton;
}
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/**
* Selected file path label.
*/
private Label getSelectedFileLabel()
{
if(mSelectedFileLabel == null)
{
mSelectedFileLabel = new Label(" ");
}
return mSelectedFileLabel;
}
/**
* Search text filter box
* @return text control for entering search text
*/
private TextField getSearchText()
{
if(mSearchText == null)
{
mSearchText = new TextField();
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mSearchText;
}
/**
* Text box for find text
*/
private TextField getFindText()
{
if(mFindText == null)
{
mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mFindText;
}
/**
* Find button to search for the text in the find text box.
* @return button
*/
private Button getFindButton()
{
if(mFindButton == null)
{
mFindButton = new Button("Find");
mFindButton.setOnAction(event -> find(getFindText().getText()));
}
return mFindButton;
}
/**
* Find next button to search for the text in the find text box.
* @return button
*/
private Button getFindNextButton()
{
if(mFindNextButton == null)
{
mFindNextButton = new Button("Next");
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
}
return mFindNextButton;
}
}

View File

@@ -0,0 +1,180 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
* Supported Protocols: DMR and APCO25 Phase 1.
*/
public class RecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
/**
* Constructs an instance
*/
public RecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
}
public MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
Menu createNewViewerMenu = new Menu("New Viewer ...");
MenuItem dmrMenuItem = new MenuItem("DMR");
dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())));
MenuItem p25p1MenuItem = new MenuItem("P25P1");
p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())));
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
fileMenu.getItems().addAll(createNewViewerMenu, new SeparatorMenuItem(), exitMenu);
mMenuBar.getMenus().add(fileMenu);
}
return mMenuBar;
}
/**
* Tab pane for each viewer instance
*/
public TabPane getTabPane()
{
if(mTabPane == null)
{
mTabPane = new TabPane();
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
}
return mTabPane;
}
/**
* Decorates the tab with label renaming feature using double-click or right-click to change text
*/
public class LabeledTab extends Tab
{
private Label mLabel = new Label();
private TextField mTextField = new TextField();
/**
* Constructs an instance
* @param label to use initially
* @param node for content
*/
public LabeledTab(String label, Node node)
{
super(null, node);
mLabel.setText(label);
setGraphic(mLabel);
mLabel.setOnMouseClicked(event -> {
if((event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) ||
event.getButton().equals(MouseButton.SECONDARY))
{
setGraphic(mTextField);
mTextField.setText(mLabel.getText());
mTextField.selectAll();
mTextField.requestFocus();
}
});
mTextField.setOnAction(event -> {
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
});
mTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue)
{
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
}
});
}
public LabeledTab(String label)
{
this(label, null);
}
}
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting message recording viewer application", e);
}
};
Platform.startup(r);
}
}

View File

@@ -17,17 +17,17 @@
* ****************************************************************************
*/
package io.github.dsheirer.gui.dmr;
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.gui.JavaFxWindowRequest;
/**
* Request to show the DMR recording viewer
* Request to show the recording viewer
*/
public class ViewDmrRecordingViewerRequest extends JavaFxWindowRequest
public class ViewRecordingViewerRequest extends JavaFxWindowRequest
{
/**
* Constructs an instance
*/
public ViewDmrRecordingViewerRequest() {}
public ViewRecordingViewerRequest() {}
}

View File

@@ -1,69 +1,161 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * 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/>
* * *****************************************************************************
* 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.identifier.patch;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Temporary grouping or regrouping of talkgroups or individual radios.
*/
public class PatchGroup
{
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
private TalkgroupIdentifier mPatchGroupIdentifier;
private List<TalkgroupIdentifier> mPatchedGroupIdentifiers = new ArrayList<>();
private List<TalkgroupIdentifier> mPatchedTalkgroupIdentifiers = new ArrayList<>();
private List<RadioIdentifier> mPatchedRadioIdentifiers = new ArrayList<>();
private int mVersion;
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
/**
* Constructs an instance
* @param patchGroupIdentifier for the patch group
* @param version for this patch group identifier. Note: if the patch group identifier is frequently reused, a
* new version number will indicate a new instance of the patch group.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier, int version)
{
mPatchGroupIdentifier = patchGroupIdentifier;
mVersion = version;
}
/**
* Constructs an instance
* @param patchGroupIdentifier that is the overall identifier for this temporary grouping.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
{
this(patchGroupIdentifier, 0);
}
/**
* Version number for this patch group used to differentiate frequently reused patch group identifiers.
* @return version number (aka super group sequence number).
*/
public int getVersion()
{
return mVersion;
}
/**
* Patch group identifier.
* @return identifier
*/
public TalkgroupIdentifier getPatchGroup()
{
return mPatchGroupIdentifier;
}
public void addPatchedGroup(TalkgroupIdentifier patchedGroupIdentifier)
/**
* Indicates if this patch group contains any patched talkgroups.
* @return true if there are patched talkgroups.
*/
public boolean hasPatchedTalkgroups()
{
if(!mPatchedGroupIdentifiers.contains(patchedGroupIdentifier))
return !getPatchedTalkgroupIdentifiers().isEmpty();
}
/**
* Indicates if this patch group contains any patched radio identifiers.
* @return true if there are patched radios.
*/
public boolean hasPatchedRadios()
{
return !getPatchedRadioIdentifiers().isEmpty();
}
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{
mPatchedGroupIdentifiers.add(patchedGroupIdentifier);
mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
}
}
public void addPatchedGroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
/**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{
addPatchedGroup(identifier);
addPatchedTalkgroup(identifier);
}
}
public List<TalkgroupIdentifier> getPatchedGroupIdentifiers()
/**
* List of patched group identifiers that are part of this patch group.
* @return patched talkgroups.
*/
public List<TalkgroupIdentifier> getPatchedTalkgroupIdentifiers()
{
return mPatchedGroupIdentifiers;
return mPatchedTalkgroupIdentifiers;
}
/**
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
}
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
}
}
/**
* List of patched radio identifiers that are part of this patch group.
* @return patched radios.
*/
public List<RadioIdentifier> getPatchedRadioIdentifiers()
{
return mPatchedRadioIdentifiers;
}
@Override
@@ -72,9 +164,14 @@ public class PatchGroup
StringBuilder sb = new StringBuilder();
sb.append("P:").append(getPatchGroup());
if(!getPatchedGroupIdentifiers().isEmpty())
if(hasPatchedTalkgroups())
{
sb.append(" ").append(getPatchedGroupIdentifiers());
sb.append(" ").append(getPatchedTalkgroupIdentifiers());
}
if(hasPatchedRadios())
{
sb.append(" ").append(getPatchedRadioIdentifiers());
}
return sb.toString();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,27 +22,23 @@ package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Manager for (temporary) patch groups and related identifiers.
* Manager for (temporary) patch groups aka super groups. This manager monitors patch group additions and deletions and
* maintains a map of the current state of each patch group active in the system.
*
* Patch groups are primarily used on Motorola P25 systems. This manager monitors patch group additions and deletions
* and maintains a map of the current patch groups.
*
* Traffic channel calls will sometimes reference a patch group by simply using the patch group identifier. This
* manager will replace that reference with the full patch group so that the call event has the full patch group
* including all of the patched groups, which may not have been included in the patch group reference on the
* traffic channel.
* Traffic channel calls will reference a patch group by simply using the patch group identifier. This
* manager will replace that reference with the current state of the full patch group so that the call event has the
* full patch group including all patched talkgroups or individual radios, which may not have been included in the
* patch group reference on the control or traffic channel.
*/
public class PatchGroupManager
{
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroupManager.class);
private Map<Integer,Long> mPatchGroupLastUpdatedMap = new HashMap<>();
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
/**
@@ -58,13 +54,13 @@ public class PatchGroupManager
public void clear()
{
mPatchGroupMap.clear();
mPatchGroupLastUpdatedMap.clear();
}
/**
* Adds the patch group identifier to the set of managed patch groups. If the patch group is already being
* managed, the new identifier is checked for additional patched talkgroups and they are added to the existing
* managed patch group.
* managed, the new patch group argument is checked for additional patched talkgroups and they are added to the
* existing managed patch group. Where the patch group version (super group sequence number) is different, the
* existing patch group is discarded and replaced with the updated version of the patch group.
*
* @param patchGroupIdentifier to add or update
*/
@@ -76,8 +72,19 @@ public class PatchGroupManager
{
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
{
PatchGroup current = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
current.addPatchedGroups(update.getPatchedGroupIdentifiers());
PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
//If the patch group version number is the same, this is an update
if(existingPatchGroup.getVersion() == update.getVersion())
{
existingPatchGroup.addPatchedTalkgroups(update.getPatchedTalkgroupIdentifiers());
existingPatchGroup.addPatchedRadios(update.getPatchedRadioIdentifiers());
}
//Otherwise, this is a replace operation.
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
}
}
else
{
@@ -109,7 +116,6 @@ public class PatchGroupManager
{
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
mPatchGroupMap.remove(id);
mPatchGroupLastUpdatedMap.remove(id);
}
/**
@@ -153,23 +159,28 @@ public class PatchGroupManager
*/
public Identifier update(Identifier identifier)
{
if(identifier == null)
{
return identifier;
}
if(identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{
switch(identifier.getForm())
{
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier)
case TALKGROUP:
if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
Identifier mapValue = mPatchGroupMap.get(talkgroupIdentifier.getValue());
if (mapValue != null)
{
return mapValue;
}
}
break;
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
if (mapValue != null) {
if (mapValue != null)
{
return mapValue;
}
}

View File

@@ -145,6 +145,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
{
BW_3_0("3.0 kHz", 3000.0),
BW_5_0("5.0 kHz", 5000.0),
BW_7_5("7.5 kHz", 7500.0),
BW_8_33("8.33 kHz", 8333.0),
BW_12_5("12.5 kHz", 12500.0),
BW_15_0("15.0 kHz", 15000.0),
@@ -168,7 +169,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0);
//FM demodulator channel bandwidth options
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0);
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_7_5, BW_12_5, BW_25_0);
/**
* Indicates if this entry is valid for the AM decoder.

View File

@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -74,19 +75,24 @@ import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.Arc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
import io.github.dsheirer.util.PacketUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -105,7 +111,7 @@ public class DMRDecoderState extends TimeslotDecoderState
private DecodeEvent mCurrentCallEvent;
private long mCurrentFrequency;
private boolean mIgnoreCRCChecksums;
private Map<Integer,DecodeEvent> mDetectedCallEventsMap = new TreeMap<>();
private Map<DMRChannel,DecodeEvent> mDetectedCallEventsMap = new HashMap<>();
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
new AddChannelRotationActiveStateRequest(State.ACTIVE);
@@ -320,6 +326,46 @@ public class DMRDecoderState extends TimeslotDecoderState
.build();
broadcast(smsEvent);
}
//Hytera Radio Registration Service (RRS)
else if(packet.getPacket() instanceof HyteraRrsPacket rrs)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
StringBuilder sb = new StringBuilder();
sb.append("HYTERA RRS REGISTER RADIO:");
sb.append(rrs.getDestination());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(sb.toString())
.build();
broadcast(shortDataEvent);
}
//Hytera Short Data
else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
StringBuilder sb = new StringBuilder();
sb.append("HYTERA");
if(hsdp.getPacketSequence().isEncrypted())
{
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)hsdp.getPacketSequence().getProprietaryDataHeader();
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
sb.append(" KEY:").append(hdeh.getKeyId());
sb.append(" IV:").append(hdeh.getIV());
}
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(sb.toString())
.build();
broadcast(shortDataEvent);
}
//Unknown Hytera Long Data Service Token Message
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
{
@@ -328,32 +374,69 @@ public class DMRDecoderState extends TimeslotDecoderState
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.build();
broadcast(unknownTokenEvent);
}
//Motorola ARS
else if(packet.getPacket() instanceof ARSPacket ars)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(ars.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola LRRP
else if(packet.getPacket() instanceof LRRPPacket lrrp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(lrrp.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola XCMP
else if(packet.getPacket() instanceof XCMPPacket xcmp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(xcmp.toString())
.build();
broadcast(shortDataEvent);
}
else
{
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.timeslot(getTimeslot())
.details(packet.toString())
.build();
broadcast(packetEvent);
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
}
if (geoPosition != null) {
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
broadcast(plottableDecodeEvent);
}
if (geoPosition != null) {
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
broadcast(plottableDecodeEvent);
}
}
@@ -380,7 +463,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
if(embedded.getShortBurst() instanceof Arc4EncryptionParameters arc4)
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
{
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
}
@@ -653,21 +736,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ChannelGrant dataGrant = (ChannelGrant)csbk;
DMRChannel channel = dataGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager())
{
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted());
}
else
{
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
mDetectedCallEventsMap.put(channel.getValue(), event);
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
}
else
{
@@ -686,21 +770,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ChannelGrant tgGrant = (ChannelGrant)csbk;
DMRChannel channel = tgGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager())
{
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
}
else
{
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
mDetectedCallEventsMap.put(channel.getValue(), event);
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
}
else
{
@@ -718,21 +803,23 @@ public class DMRDecoderState extends TimeslotDecoderState
{
ChannelGrant channelGrant = (ChannelGrant)csbk;
DMRChannel channel = channelGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager())
{
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
}
else
{
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel, mergedIdentifiers);
mDetectedCallEventsMap.put(channel.getValue(), event);
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel,
new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
}
else
{
@@ -763,21 +850,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
DMRChannel channel = cpdcg.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager())
{
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted());
}
else
{
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
mDetectedCallEventsMap.put(channel.getValue(), event);
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
}
else
{
@@ -793,7 +881,7 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
.details("Registration Request")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(getTimeslot())
.build();
broadcast(event);
@@ -802,7 +890,7 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
.details("Registration Response")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(getTimeslot())
.build();
broadcast(regRespEvent);
@@ -814,21 +902,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
DMRChannel channel = cpvcu.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager())
{
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted());
}
else
{
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel.getValue());
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel);
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
{
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
mDetectedCallEventsMap.put(channel.getValue(), detectedEvent);
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, detectedEvent);
}
else
{
@@ -844,7 +933,7 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
.details("TALKGROUP AFFILIATION")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(getTimeslot())
.build();
broadcast(affiliateEvent);
@@ -857,18 +946,18 @@ public class DMRDecoderState extends TimeslotDecoderState
}
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel,
IdentifierCollection mergedIdentifiers) {
IdentifierCollection identifierCollection) {
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
.channel(channel)
.details(csbk.getOpcode().getLabel())
.identifiers(mergedIdentifiers)
.identifiers(identifierCollection)
.timeslot(getTimeslot())
.build();
}
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(getTimeslot())
.details(details)
.build();
@@ -972,6 +1061,15 @@ public class DMRDecoderState extends TimeslotDecoderState
{
switch(message.getOpcode())
{
case FULL_ENCRYPTION_PARAMETERS:
if(message instanceof io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters ep)
{
if(mCurrentCallEvent != null)
{
mCurrentCallEvent.setDetails(ep.getDetails());
}
}
break;
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
if(message instanceof CapacityPlusRestChannel)
{
@@ -1112,19 +1210,17 @@ public class DMRDecoderState extends TimeslotDecoderState
}
break;
case FULL_STANDARD_GPS_INFO:
if(message instanceof GPSInformation)
if(message instanceof GPSInformation gpsInformation)
{
GPSInformation gpsInformation = (GPSInformation)message;
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
ic.update(gpsInformation.getGPSLocation());
PlottableDecodeEvent plottableGPS = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
.channel(getCurrentChannel())
.details("LOCATION:" + gpsInformation.getGPSLocation())
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.protocol(Protocol.DMR)
.location(gpsInformation.getPosition())
.build();
DecodeEvent gpsEvent = DMRDecodeEvent.builder(DecodeEventType.GPS, message.getTimestamp())
.identifiers(ic)
.timeslot(getTimeslot())
.details("LOCATION:" + gpsInformation.getGPSLocation())
.build();
broadcast(gpsEvent);
broadcast(plottableGPS);
}
break;
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
@@ -1141,7 +1237,7 @@ public class DMRDecoderState extends TimeslotDecoderState
* @param encryptionParameters decoded from the Voice Frame F
* @param isGroup true for group or false for individual call.
*/
private void updateEncryptedCall(Arc4EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
{
if(mCurrentCallEvent != null)
{
@@ -1151,7 +1247,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
details = encryptionParameters.toString();
}
else if(!details.contains(encryptionParameters.toString()))
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
{
details += " " + encryptionParameters;
}

View File

@@ -127,19 +127,19 @@ public class DMRMessageProcessor implements Listener<IMessage>
}
}
//Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
if(message instanceof ITimeslotFrequencyReceiver)
{
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
int[] lsns = receiver.getLogicalSlotNumbers();
int[] lcns = receiver.getLogicalChannelNumbers();
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
for(int lsn: lsns)
for(int lcn: lcns)
{
if(mTimeslotFrequencyMap.containsKey(lsn))
if(mTimeslotFrequencyMap.containsKey(lcn))
{
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn));
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* 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;
@@ -41,14 +38,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusCo
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
import io.github.dsheirer.module.decode.dmr.message.type.Model;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks the network configuration details of a DMR network from the broadcast messages
@@ -250,7 +246,7 @@ public class DMRNetworkConfigurationMonitor
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
mNeighborSites.addAll(cpnr.getNeighbors());
}
if(mBrand == null)
if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
{
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
}

View File

@@ -98,7 +98,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
private List<Channel> mManagedTrafficChannels;
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
private Map<Integer,DMRChannelGrantEvent> mLSNGrantEventMap = new ConcurrentHashMap<>();
private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
private Listener<ChannelEvent> mChannelEventListener;
private Listener<IDecodeEvent> mDecodeEventListener;
@@ -322,9 +322,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
Opcode opcode, long timestamp, boolean encrypted)
{
int lsn = channel.getValue();
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
if(isStale(event, timestamp, identifierCollection)) //Create new event
@@ -335,7 +333,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
.identifiers(identifierCollection)
.build();
mLSNGrantEventMap.put(lsn, event);
mChannelGrantEventMap.put(channel, event);
}
else //Update current event
@@ -355,7 +353,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
.identifiers(identifierCollection)
.build();
mLSNGrantEventMap.put(lsn, event);
mChannelGrantEventMap.put(channel, event);
broadcast(event);
}
}
@@ -643,37 +641,37 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
*/
private void removeCallEvents(long frequency)
{
List<Integer> lsnsToRemove = new ArrayList<>();
List<DMRChannel> channelsToRemove = new ArrayList<>();
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
{
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
if(entry.getKey().getDownlinkFrequency() == frequency)
{
lsnsToRemove.add(entry.getKey());
channelsToRemove.add(entry.getKey());
}
}
for(Integer lsn: lsnsToRemove)
for(DMRChannel channel: channelsToRemove)
{
mLSNGrantEventMap.remove(lsn);
mChannelGrantEventMap.remove(channel);
}
}
private void updateCallEventDetails(long frequency, String detailFragment)
{
List<Integer> lsnsToUpdate = new ArrayList<>();
List<DMRChannel> channelsToUpdate = new ArrayList<>();
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
{
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
{
lsnsToUpdate.add(entry.getKey());
channelsToUpdate.add(entry.getKey());
}
}
for(Integer lsn: lsnsToUpdate)
for(DMRChannel channel: channelsToUpdate)
{
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
if(event != null)
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,7 +38,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences;
@@ -105,63 +104,54 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
{
if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
{
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
if(!mEncryptedCallStateEstablished && message instanceof VoiceMessage vm &&
vm.getSyncPattern().isDirectMode())
//Attempt to set the audio encryption state from certain types of messages
if(!mEncryptedCallStateEstablished)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = false;
List<byte[]> frames = vm.getAMBEFrames();
for(byte[] frame: frames)
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
{
processAudio(frame, message.getTimestamp());
mEncryptedCallStateEstablished = true;
mEncryptedCall = false;
}
}
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
//in the EMB field.
else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage voiceMessage)
{
if(voiceMessage.getEMB().isValid())
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
//in the EMB field.
else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = voiceMessage.getEMB().isEncrypted();
}
else if(message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
{
if(flc.isValid())
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = flc.isEncrypted();
if(flc.isValid())
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = flc.isEncrypted();
}
}
}
//Note: the DMRMessageProcessor extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
else if(message instanceof AbstractVoiceChannelUser avcu)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = avcu.getServiceOptions().isEncrypted();
}
}
//Note: the DMRMessageProcess extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
else if(!mEncryptedCallStateEstablished && message instanceof AbstractVoiceChannelUser avcu)
//Queue or process audio frames. Note: audio frames are held/queued until encrypted state is established
//before any audio is generated for the audio segment.
if(message instanceof VoiceMessage voiceMessage)
{
ServiceOptions serviceOptions = avcu.getServiceOptions();
mEncryptedCallStateEstablished = true;
mEncryptedCall = serviceOptions.isEncrypted();
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(message instanceof Terminator)
{

View File

@@ -26,6 +26,8 @@ import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.Validate;
/**
@@ -33,8 +35,9 @@ import org.apache.commons.lang3.Validate;
*
* Note: timeslots are tracked as 1 and 2
*/
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
{
private TimeslotFrequency mTimeslotFrequency;
private int mTimeslot;
/**
@@ -56,13 +59,23 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
}
/**
* Repeater number or channel number for the channel.
* Repeater number aka Logical Channel Number (LCN) for the channel.
*/
public int getChannel()
public int getChannelNumber()
{
return getValue();
}
/**
* Convenience method to format the single channel number as an array for TimeslotFrequency matching.
* @return channel number as an integer array.
*/
public int[] getLogicalChannelNumbers()
{
return new int[]{getChannelNumber()};
}
/**
* Timeslot for the channel.
* @return timeslot as zero-based index with values in range: 0 or 1.
@@ -95,7 +108,7 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
@Override
public String toString()
{
return "CHAN:" + getChannel() + ":" + getTimeslot();
return "CHAN:" + getChannelNumber() + ":" + getTimeslot();
}
/**
@@ -115,4 +128,72 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
{
throw new IllegalArgumentException("This method is not supported");
}
@Override
public long getDownlinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
/**
* Sets the lsn to frequency mapper value.
* @param timeslotFrequency to set
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
}
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(timeslotFrequency.getChannelNumber() == getChannelNumber())
{
setTimeslotFrequency(timeslotFrequency);
return;
}
}
}
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(!(o instanceof DMRChannel that))
{
return false;
}
if(!super.equals(o))
{
return false;
}
return getTimeslot() == that.getTimeslot();
}
@Override
public int hashCode()
{
return Objects.hash(super.hashCode(), getTimeslot());
}
}

View File

@@ -19,8 +19,6 @@
package io.github.dsheirer.module.decode.dmr.channel;
import java.util.List;
/**
* DMR Logical Slot Number (LSN) channel.
*
@@ -44,10 +42,8 @@ import java.util.List;
* 15: 8/1
* 16: 8/2
*/
public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
public class DMRLsn extends DMRChannel
{
private TimeslotFrequency mTimeslotFrequency;
/**
* Constructs an instance
*
@@ -61,7 +57,7 @@ public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
@Override
public String toString()
{
return "LSN:" + getLsn();
return "LSN:" + getLsn() + " LCN:" + getChannelNumber();
}
/**
@@ -70,65 +66,7 @@ public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
*/
public int getLsn()
{
return ((getChannel() - 1) * 2) + getTimeslot();
return ((getChannelNumber() - 1) * 2) + getTimeslot();
}
@Override
public long getDownlinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
@Override
public int[] getLogicalSlotNumbers()
{
return new int[]{getLsn()};
}
/**
* Sets the lsn to frequency mapper value.
* @param timeslotFrequency to set
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
}
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(timeslotFrequency.getNumber() == getLsn())
{
setTimeslotFrequency(timeslotFrequency);
return;
}
}
}
public static void main(String[] args)
{
for(int x = 1; x <= 16; x++)
{
DMRLsn lsn = new DmrRestLsn(x);
System.out.println(lsn);
}
}
}

View File

@@ -24,14 +24,12 @@ package io.github.dsheirer.module.decode.dmr.channel;
*/
public class DMRTier3Channel extends DMRChannel
{
private TimeslotFrequency mTimeslotFrequency;
/**
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
* calculated logical slot value for visual compatibility for users.
*
* @param channel number or repeater number
* @param logicalSlotNumber - zero based index.
* @param timeslot either 1 or 2.
*/
public DMRTier3Channel(int channel, int timeslot)
{
@@ -39,42 +37,12 @@ public class DMRTier3Channel extends DMRChannel
}
/**
* Downlink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
* Tier III Channel ID
* @return channel ID
*/
@Override
public long getDownlinkFrequency()
public int getChannelId()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
/**
* Uplink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
*/
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
/**
* Sets the timeslot frequency mapping
* @param timeslotFrequency
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
return getChannelNumber() * 2 + getTimeslot();
}
/**
@@ -83,7 +51,8 @@ public class DMRTier3Channel extends DMRChannel
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(" LCN:").append(getValue());
sb.append(" LCN:").append(getChannelNumber());
sb.append(" CHANID:").append(getChannelId());
return sb.toString();
}
}

View File

@@ -24,9 +24,9 @@ import java.util.List;
public interface ITimeslotFrequencyReceiver
{
/**
* Provides the logical slot number(s) that require a matching timeslot frequency mapping
* Provides the logical channel number(s) that require a matching timeslot frequency mapping
*/
public int[] getLogicalSlotNumbers();
public int[] getLogicalChannelNumbers();
/**
* Applies the list of timeslot frequency mappings to the implementer

View File

@@ -31,7 +31,7 @@ import javafx.beans.property.SimpleLongProperty;
import javafx.util.Callback;
/**
* Maps a logical slot number (LSN) to a pair of channel frequency values
* Maps a logical channel number (LCN) to a pair of channel frequency values
*/
public class TimeslotFrequency
{
@@ -100,7 +100,10 @@ public class TimeslotFrequency
}
/**
* Logical slot number (LSN) as a 1-index based counter
* Logical channel number (LCN) as a 1-index based counter
*
* Note: lsn (logical slot number) is a legacy name for this field. The field currently holds the LCN or logical
* channel number. To avoid breaking serialization, we leave this field labeled as lsn.
*/
@JacksonXmlProperty(isAttribute = true, localName = "lsn")
public int getNumber()
@@ -109,12 +112,52 @@ public class TimeslotFrequency
}
/**
* Sets the logical slot number (LSN) as a 1-index start
* @param lsn where LSN 1 is the first slot, 2 the second, etc
* Logical channel number which is the same value as getNumber.
* @return
*/
public void setNumber(int lsn)
@JsonIgnore
public int getChannelNumber()
{
mNumberProperty.set(lsn);
return getNumber();
}
@JsonIgnore
public String getLogicalSlotNumbers()
{
StringBuilder sb = new StringBuilder();
int channel = getChannelNumber();
if(channel > 0)
{
if(channel < 32) //Connect Plus uses 5-bits for LSN for a maximum value of 31
{
//Cap+ & Con+ Logical Slot Numbers
int lsn1 = (channel - 1) * 2 + 1;
int lsn2 = (channel - 1) * 2 + 2;
sb.append("LSN:").append(lsn1).append("/").append(lsn2).append(" or ");
}
//Tier III channel IDs
int chanId1 = channel * 2 + 1;
int chanId2 = channel * 2 + 2;
sb.append("Tier3 Chan:").append(chanId1).append("/").append(chanId2);
}
else
{
sb.append("(empty)");
}
return sb.toString();
}
/**
* Sets the logical channel number (LCN) as a 1-index start
* @param lcn where LCN 1 is logical slot numbers (LSN) 1 and 2, LCN2=LSN3/4, etc.
*/
public void setNumber(int lcn)
{
mNumberProperty.set(lcn);
}
/**
@@ -160,7 +203,7 @@ public class TimeslotFrequency
@Override
public String toString()
{
return "TIMESLOT LSN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
return "LCN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
}
/**

View File

@@ -33,8 +33,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory
import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.PiHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader;
@@ -43,7 +43,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.StatusDataHeader
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
@@ -98,7 +98,7 @@ public class DMRDataMessageFactory
return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
return new HeaderMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
return new PiHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case VOICE_HEADER:
return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case DATA_ENC_HEADER:
@@ -138,10 +138,10 @@ public class DMRDataMessageFactory
return mprdh;
}
case HYTERA_68:
HyteraProprietaryDataHeader hpdh = new HyteraProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hpdh.setValid(valid);
return hpdh;
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);

View File

@@ -34,7 +34,7 @@ import java.util.List;
*/
public abstract class DataMessageWithLinkControl extends DataMessage
{
private LCMessage mLCMessage;
protected LCMessage mLCMessage;
/**
* Constructs an instance.

View File

@@ -56,6 +56,10 @@ public class DataBlock1Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -56,6 +56,10 @@ public class DataBlock1_2Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -57,6 +57,10 @@ public class DataBlock3_4Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/**
* Control Signalling Block (CSBK) Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B
*/
@@ -91,6 +90,8 @@ public enum Opcode
HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"),
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"),
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
//and 47 was only transmitted 3x times in succession in the middle of the terminators and CSBKO 44 messages.
HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"),
HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
@@ -104,9 +105,9 @@ public enum Opcode
UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN");
private Vendor mVendor;
private int mValue;
private String mLabel;
private final Vendor mVendor;
private final int mValue;
private final String mLabel;
/**
* Constructor

View File

@@ -299,9 +299,9 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
}
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getRestChannel().getLogicalSlotNumbers();
return getRestChannel().getLogicalChannelNumbers();
}
/**
@@ -312,13 +312,7 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(timeslotFrequency.getNumber() == getRestChannel().getValue())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getRestChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -299,9 +299,9 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
* Logical slot numbers that require slot to frequency mappings.
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getRestChannel().getLogicalSlotNumbers();
return getRestChannel().getLogicalChannelNumbers();
}
/**
@@ -312,13 +312,7 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getRestChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -40,7 +40,7 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
{
private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] REPEATER = new int[]{40, 41, 42, 43, 44};
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{40, 41, 42, 43, 44};
//Analysis: this field correlates to UNKNOWN_FIELD_1(bits: 40-48) in ConnectPlusTerminateChannelGrant.
private static final int[] UNKNOWN_FIELD = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
@@ -109,11 +109,11 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
}
/**
* Channel grant repeater number
* Channel grant logical slot number
*/
public int getRepeater()
public int getLsn()
{
return getMessage().getInt(REPEATER);
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Don't change this ... always subtract 1 to get the repeater LSN
}
/**
@@ -123,16 +123,16 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
{
if(mDmrLsn == null)
{
mDmrLsn = new DMRLsn(getRepeater());
mDmrLsn = new DMRLsn(getLsn());
}
return mDmrLsn;
}
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getChannel().getLogicalSlotNumbers();
return getChannel().getLogicalChannelNumbers();
}
/**
@@ -142,13 +142,7 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(timeslotFrequency.getNumber() == getChannel().getValue())
{
getChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -45,7 +45,7 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
private static final int[] VERSION = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] UNKNOWN = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62};
private static final int[] DATA_REPEATER = new int[]{63, 64, 65, 66, 67};
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{63, 64, 65, 66, 67};
private DMRLsn mDataChannel;
private List<Identifier> mIdentifiers;
@@ -99,11 +99,11 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
}
/**
* Data repeater number
* Data logical slot numberrepeater number
*/
public int getDataRepeater()
public int getDataLsn()
{
return getMessage().getInt(DATA_REPEATER);
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Leave this as minus 1
}
/**
@@ -113,16 +113,16 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
{
if(mDataChannel == null)
{
mDataChannel = new DMRLsn(getDataRepeater());
mDataChannel = new DMRLsn(getDataLsn());
}
return mDataChannel;
}
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getDataChannel().getLogicalSlotNumbers();
return getDataChannel().getLogicalChannelNumbers();
}
/**
@@ -133,13 +133,7 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(timeslotFrequency.getNumber() == getDataChannel().getValue())
{
getDataChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getDataChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -44,7 +44,7 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] TRAFFIC_CHANNEL_REPEATER = new int[]{64, 65, 66, 67, 68};
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{64, 65, 66, 67, 68};
private static final int[] UNKNOWN_FIELD = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
@@ -126,9 +126,9 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
/**
* Traffic channel repeater
*/
public int getTrafficChannelRepeater()
public int getTrafficLsn()
{
return getMessage().getInt(TRAFFIC_CHANNEL_REPEATER);
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Always leave as minus 1
}
/**
@@ -138,16 +138,16 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
{
if(mDmrLsn == null)
{
mDmrLsn = new DMRLsn(getTrafficChannelRepeater());
mDmrLsn = new DMRLsn(getTrafficLsn());
}
return mDmrLsn;
}
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getChannel().getLogicalSlotNumbers();
return getChannel().getLogicalChannelNumbers();
}
/**
@@ -157,13 +157,7 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(timeslotFrequency.getNumber() == getChannel().getValue())
{
getChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -224,11 +223,11 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
* Logical Slot Number(s) for channels contained in this message
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
if(getMoveToChannel() instanceof DMRLsn dmrLsn)
if(getMoveToChannel() != null)
{
return dmrLsn.getLogicalSlotNumbers();
return getMoveToChannel().getLogicalChannelNumbers();
}
return new int[0];
@@ -241,17 +240,9 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
if(getMoveToChannel() instanceof DMRTier3Channel)
if(getMoveToChannel() != null)
{
DMRTier3Channel channel = (DMRTier3Channel)getMoveToChannel();
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getValue() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
getMoveToChannel().apply(timeslotFrequencies);
}
}

View File

@@ -88,7 +88,8 @@ public class MoveTSCC extends CSBKMessage
if(multiBlock != null)
{
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
//Timeslot hard-coded to 1 since this is a control channel.
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
}
}

View File

@@ -84,8 +84,8 @@ public class AdjacentSiteInformation extends Announcement
if(multiBlock != null)
{
//Timeslot hard-coded to zero for control channel
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
//Timeslot hard-coded to one for control channel
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,7 +27,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.ArrayList;
import java.util.List;
@@ -56,7 +55,8 @@ public class AnnounceChannelFrequency extends Announcement
if(multiBlock != null)
{
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
//Timeslot hard-coded to one
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
}
}

View File

@@ -86,8 +86,8 @@ public class AnnounceWithdrawTSCC extends Announcement
if(multiBlock != null)
{
//Control channel timeslot hard-coded to 0
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
//Control channel timeslot hard-coded to 1
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
}
}

View File

@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -90,7 +89,8 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
if(multiBlock != null)
{
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
//Timeslot hard-coded to one for control channel
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
}
}
@@ -239,11 +239,11 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
* Logical Slot Number(s) for channels contained in this message
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
if(getChannel() instanceof DMRLsn dmrLsn)
if(getChannel() != null)
{
return dmrLsn.getLogicalSlotNumbers();
return getChannel().getLogicalChannelNumbers();
}
return new int[0];
@@ -256,17 +256,9 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
if(getChannel() instanceof DMRTier3Channel)
if(getChannel() != null)
{
DMRTier3Channel channel = (DMRTier3Channel)getChannel();
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getValue() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
getChannel().apply(timeslotFrequencies);
}
}
}

View File

@@ -104,7 +104,7 @@ public class BroadcastTalkgroupVoiceChannelGrant extends ChannelGrant
sb.append(" BROADCAST TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -158,11 +157,11 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
* Logical Slot Number(s) for channels contained in this message
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
if(getChannel() instanceof DMRLsn dmrLsn)
if(getChannel() != null)
{
return dmrLsn.getLogicalSlotNumbers();
return getChannel().getLogicalChannelNumbers();
}
return new int[0];
@@ -175,17 +174,9 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
if(getChannel() instanceof DMRTier3Channel)
if(getChannel() != null)
{
DMRTier3Channel channel = (DMRTier3Channel)getChannel();
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getValue() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
getChannel().apply(timeslotFrequencies);
}
}
}

View File

@@ -103,7 +103,7 @@ public class DuplexPrivateDataChannelGrant extends ChannelGrant
sb.append(" DUPLEX PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -103,7 +103,7 @@ public class DuplexPrivateVoiceChannelGrant extends ChannelGrant
sb.append(" DUPLEX PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -108,7 +108,7 @@ public class PrivateDataChannelGrant extends ChannelGrant
sb.append(" PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -102,7 +102,7 @@ public class PrivateVoiceChannelGrant extends ChannelGrant
sb.append(" PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -110,7 +110,7 @@ public class TalkgroupDataChannelGrant extends ChannelGrant
sb.append(" TALKGROUP DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -104,7 +104,7 @@ public class TalkgroupVoiceChannelGrant extends ChannelGrant
sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel());
sb.append(" ").append(getChannel());
return sb.toString();
}

View File

@@ -17,18 +17,19 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera;
package io.github.dsheirer.module.decode.dmr.message.data.header;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
/**
* Hytera Proprietary Data Header
* PI/Encryption Header with Link Control
*/
public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
public class PiHeader extends HeaderMessage
{
/**
* Constructs an instance.
@@ -40,23 +41,42 @@ public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
* @param timestamp message was received
* @param timeslot for the DMR burst
*/
public HyteraProprietaryDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{
super(syncPattern, message, cach, slotType, timestamp, timeslot);
}
/**
* Access the embedded link control message
*/
public LCMessage getLCMessage()
{
if(mLCMessage == null)
{
//Overrides default to ignore opcode and construct an encryption parameters message.
mLCMessage = LCMessageFactory.createFullEncryption(getMessage(), getTimestamp(), getTimeslot());
}
return mLCMessage;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid())
{
sb.append(" [CRC ERROR]");
}
sb.append(" HYTERA PROPRIETARY DATA HEADER");
sb.append(" SAP:").append(getServiceAccessPoint());
sb.append(" MSG:").append(getMessage().toHexString());
sb.append(getSlotType());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits()).append(" ");
}
sb.append(" ").append(getLCMessage());
return sb.toString();
}
}

View File

@@ -0,0 +1,126 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
/**
* Hytera Proprietary Data Header for SAP Short Data
*/
public class HyteraDataEncryptionHeader extends ProprietaryDataHeader
{
private static final int[] ALGORITHM = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] KEY = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] INITIALIZATION_VECTOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
/**
* Constructs an instance.
*
* @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA
* @param message containing extracted 196-bit payload.
* @param cach for the DMR burst
* @param slotType for this data message
* @param timestamp message was received
* @param timeslot for the DMR burst
*/
public HyteraDataEncryptionHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{
super(syncPattern, message, cach, slotType, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid())
{
sb.append(" [CRC ERROR]");
}
sb.append(" HYTERA DATA ENCRYPTION HEADER - SAP:").append(getServiceAccessPoint());
switch(getAlgorithm())
{
case NO_ENCRYPTION:
sb.append(" ALGORITHM:UNENCRYPTED");
break;
case UNKNOWN:
sb.append(" ALGORITHM:").append(getMessage().getInt(ALGORITHM));
break;
default:
sb.append(" ALGORITHM:").append(getAlgorithm());
break;
}
if(getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION)
{
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getIV());
sb.append(" UNK:").append(getUnknown());
}
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Encryption algorithm.
* @return algorithm or UNKNOWN if the value is not known.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getMessage().getInt(ALGORITHM));
}
/**
* Encryption key ID (1-255).
* @return key ID
*/
public int getKeyId()
{
return getMessage().getInt(KEY);
}
/**
* Hex string representation of the initialization vector
* @return IV in hex string.
*/
public String getIV()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
/**
* Unknown field.
* @return hex string from the field contents.
*/
public String getUnknown()
{
return getMessage().getHex(UNKNOWN, 4);
}
}

View File

@@ -64,8 +64,8 @@ public class MotorolaDataEncryptionHeader extends ProprietaryDataHeader
sb.append(" MOTOROLA DATA ENCRYPTION HEADER");
sb.append(" SAP:").append(getServiceAccessPoint());
sb.append(" ALGORITHM?:").append(getAlgorithm());
sb.append(" KEY?:").append(getKeyId());
sb.append(" IV?:").append(getInitializationVector());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" UNK:").append(getUnknown());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Checksum_5_DMR;
import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -33,13 +34,11 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasHead
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TerminatorData;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraArc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaArc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
@@ -61,8 +60,20 @@ public class LCMessageFactory
{
private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class);
private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR();
private static int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
private static int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
private static final int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
private static final int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
/**
* Creates a full link control message specifically for the PI_HEADER message type.
* @param message bits
* @param timestamp of the original message
* @param timeslot of the original message
* @return encryption parameters link control.
*/
public static FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
return new EncryptionParameters(message, timestamp, timeslot);
}
/**
* Creates a full link control message
@@ -123,34 +134,9 @@ public class LCMessageFactory
case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot);
break;
case FULL_ARC4_ENCRYPTION_PARAMETERS:
boolean isHytera = false;
//This is apparently now a DMR standard FLC opcode, even though it's using Motorola's vendor ID (0x10).
//As observed on a known Hytera system, it's not using the standard FLC RS-12/9/4 check, which fails.
//It's apparently using the CRC-CCITT with 0x9696 initial fill. Not sure if both Hytera and Motorola
// are using this FEC. But, by using only 16 bits for checksum versus the standad 24, they're able to
// include a full 24-bit group/radio ID in the message. For now we'll identify as either Motorola
//(using reed solomon) or Hytera (using crc-ccitt).
if(!valid)
{
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
if(bitErrors < 2)
{
valid = true;
isHytera = true;
}
}
if(isHytera)
{
flc = new HyteraArc4EncryptionParameters(message, timestamp, timeslot);
}
else
{
flc = new MotorolaArc4EncryptionParameters(message, timestamp, timeslot);
}
case FULL_ENCRYPTION_PARAMETERS:
flc = new EncryptionParameters(message, timestamp, timeslot);
valid = flc.isValid();
break;
case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot);

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/**
* Full and Short Link Control Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2
*/
@@ -49,7 +48,7 @@ public enum LCOpcode
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"),
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_ARC4_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ARC4/EP ENCRYPTION PARAMETERS"),
FULL_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ENCRYPTION PARAMETERS"),
//Cap+ opcodes from https://forums.radioreference.com/threads/understanding-capacity-plus-trunking-some-more.452566/
//FLCO 0: Group Call Maintenance
//FLCO 3: Private Call Maintenance (TermLC)
@@ -81,10 +80,10 @@ public enum LCOpcode
private static final EnumSet<LCOpcode> TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER,
FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3);
private Vendor mVendor;
private boolean mFull;
private int mValue;
private String mLabel;
private final Vendor mVendor;
private final boolean mFull;
private final int mValue;
private final String mLabel;
/**
* Constructs an instance

View File

@@ -17,29 +17,30 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
package io.github.dsheirer.module.decode.dmr.message.data.lc.full;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera ARC4/EP Encryption Parameters
* <p>
* Note: observed as FLC payload for a PI_HEADER slot type.
* Note: observed on a Hytera system that was configured as IP Site Connect compatible.
* Encryption Parameters, carried by a PI_HEADER.
*
* Note: this message repurposes the OPCODE field to carry the encryption algorithm.
*/
public class HyteraArc4EncryptionParameters extends FullLCMessage
public class EncryptionParameters extends FullLCMessage
{
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79};
//Hytera version uses CRC-CCITT with 0x9696 initial fill, bits: 80-95
//CRC-CCITT with 0x9696 initial fill, bits: 80-95
private DMRTalkgroup mTalkgroup;
private List<Identifier> mIdentifiers;
@@ -50,9 +51,23 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
* @param timestamp for the message
* @param timeslot of the message
*/
public HyteraArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
public EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
setValid(bitErrors < 2);
}
/**
* Overrides the default method for opcode to return a static value, because the opcode field is repurposed to
* be the algorithm field in both Motorola and Hytera variants of this message, both carried by the Encryption
* Header message.
* @return full encryption parameters opcode.
*/
@Override
public LCOpcode getOpcode()
{
return LCOpcode.FULL_ENCRYPTION_PARAMETERS;
}
@Override
@@ -75,7 +90,17 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC HYTERA ARC4/EP ENCRYPTION PARAMETERS");
sb.append("FLC ENCRYPTION PARAMETERS");
sb.append(" VENDOR:").append(getVendor());
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
{
//List the algorithm (aka opcod) value for unknown algorithms
sb.append(" ALGORITHM:").append(getOpcodeValue());
}
else
{
sb.append(" ALGORITHM:").append(getAlgorithm());
}
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" TALKGROUP:").append(getTalkgroup());
@@ -83,6 +108,29 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
return sb.toString();
}
/**
* Encryption parameter details
* @return details
*/
public String getDetails()
{
StringBuilder sb = new StringBuilder();
sb.append("ENCRYPTION ALGORITHM:").append(getAlgorithm());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" VENDOR:").append(getVendor());
return sb.toString();
}
/**
* Encryption algorithm. Note: this message repurposes the OPCODE field to carry the algorithm ID.
* @return algorithm or UNKNOWN.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getOpcodeValue());
}
public DMRTalkgroup getTalkgroup()
{
if(mTalkgroup == null)

View File

@@ -30,7 +30,7 @@ public abstract class FullLCMessage extends LCMessage
{
private static final int ENCRYPTION_FLAG = 0;
private static final int RESERVED = 1;
private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
protected static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,10 +24,10 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.location.LocationIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation;
import io.github.dsheirer.module.decode.dmr.message.type.PositionError;
import org.apache.commons.math3.util.FastMath;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jdesktop.swingx.mapviewer.GeoPosition;
/**
* GPS Information
@@ -81,16 +81,30 @@ public class GPSInformation extends FullLCMessage
return PositionError.fromValue(getMessage().getInt(POSITION_ERROR));
}
/**
* Latitude in decimal degrees
*/
public double getLatitude()
{
return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS;
}
/**
* Longitude in decimal degrees
*/
public double getLongitude()
{
return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS;
}
/**
* Geo-position for the lat/lon
*/
public GeoPosition getPosition()
{
return new GeoPosition(getLatitude(), getLongitude());
}
/**
* GPS location as an identifier
*/

View File

@@ -37,11 +37,12 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN_1 = new int[]{72, 73, 74, 75};
private static final int[] UNKNOWN_2 = new int[]{76, 77, 78, 79};
//FEC: 72-96
//TODO: Target and Source address fields may be full 24-bit values. How do we distinguish between Hytera repeater
//TODO: and a Hytera XPT repeater? Do the service options identify when it's XPT?
private ServiceOptions mServiceOptions;
private RadioIdentifier mSourceRadio;
protected RadioIdentifier mSourceRadio;
/**
* Constructs an instance.
@@ -110,24 +111,6 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase();
}
/**
* Unknown nibble one. This value remains consistent for the FLC between the Voice Header and the Terminator
* for calls. It does not seem to have correlation to the channel/repeater number
*/
public int getUnknownField1()
{
return getMessage().getInt(UNKNOWN_1);
}
/**
* Unknown nibble two. For each talkgroup, this field shows one value for the Voice Header and the same value but
* bit-inverted in the Terminator.
*/
public int getUnknownField2()
{
return getMessage().getInt(UNKNOWN_2);
}
/**
* Source radio address
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,9 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -32,6 +33,11 @@ import java.util.List;
*/
public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{
protected static final int[] TALKGROUP = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] SOURCE_RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 67, 68, 69, 70, 71};
private TalkgroupIdentifier mTalkgroup;
private List<Identifier> mIdentifiers;
@@ -55,26 +61,11 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
sb.append("[CRC-ERROR] ");
}
sb.append("FLC HYTERA XPT GROUP VOICE CHANNEL USER FM:");
sb.append("FLC HYTERA GROUP VOICE CHANNEL USER FM:");
sb.append(getSourceRadio());
sb.append(" TO:").append(getTalkgroup());
if(isAllChannelsBusy())
{
sb.append(" ALL REPEATERS BUSY");
}
else
{
sb.append(" FREE REPEATER:").append(getFreeRepeater());
}
if(hasPriorityCall())
{
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@@ -85,12 +76,25 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{
if(mTalkgroup == null)
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS));
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP));
}
return mTalkgroup;
}
/**
* Source radio address
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override
public List<Identifier> getIdentifiers()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -73,8 +72,6 @@ public class HyteraTerminator extends HyteraFullLC
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import java.util.ArrayList;
import java.util.List;
@@ -72,8 +71,6 @@ public class HyteraUnitToUnitVoiceChannelUser extends HyteraFullLC
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
sb.append(" ").append(getServiceOptions());
return sb.toString();
}

View File

@@ -21,13 +21,13 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions;
/**
* Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options.
*/
public abstract class CapacityPlusVoiceChannelUser extends FullLCMessage implements IServiceOptionsProvider
public abstract class CapacityPlusVoiceChannelUser extends AbstractVoiceChannelUser implements IServiceOptionsProvider
{
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
//Reed Solomon FEC: 72-95

View File

@@ -176,9 +176,9 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getRestChannel().getLogicalSlotNumbers();
return getRestChannel().getLogicalChannelNumbers();
}
/**
@@ -189,12 +189,6 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getRestChannel().apply(timeslotFrequencies);
}
}

View File

@@ -1,116 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola ARC4/EP Encryption Parameters
* <p>
* Note: observed as FLC payload for a PI_HEADER slot type.
* Note: observed on a possible Hytera (clone) system that was configured as IP Site Connect compatible.
*/
public class MotorolaArc4EncryptionParameters extends FullLCMessage
{
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
//Motorola Reed Solomon FEC bits: 72-95
private DMRTalkgroup mTalkgroup;
private List<Identifier> mIdentifiers;
/**
* Constructor
* @param message bits
* @param timestamp for the message
* @param timeslot of the message
*/
public MotorolaArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA ARC4/EP ENCRYPTION PARAMETERS");
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" TALKGROUP:").append(getTalkgroup());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
public DMRTalkgroup getTalkgroup()
{
if(mTalkgroup == null)
{
mTalkgroup = new DMRTalkgroup(getMessage().getInt(DESTINATION_GROUP));
}
return mTalkgroup;
}
public int getKeyId()
{
return getMessage().getInt(KEY_ID);
}
public String getInitializationVector()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
}
return mIdentifiers;
}
}

View File

@@ -105,9 +105,9 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
*/
@Override
public int[] getLogicalSlotNumbers()
public int[] getLogicalChannelNumbers()
{
return getRestChannel().getLogicalSlotNumbers();
return getRestChannel().getLogicalChannelNumbers();
}
/**
@@ -117,13 +117,7 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
getRestChannel().apply(timeslotFrequencies);
}
@Override

View File

@@ -24,7 +24,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList;
import java.util.List;
@@ -33,12 +35,12 @@ import java.util.List;
*/
public class PacketSequence
{
private int mTimeslot;
private final int mTimeslot;
private PacketSequenceHeader mPacketSequenceHeader;
private UDTHeader mUDTHeader;
private ProprietaryDataHeader mProprietaryDataHeader;
private List<Preamble> mPreambles = new ArrayList<>();
private List<DataBlock> mDataBlocks = new ArrayList<>();
private final List<Preamble> mPreambles = new ArrayList<>();
private final List<DataBlock> mDataBlocks = new ArrayList<>();
public PacketSequence(int timeslot)
{
@@ -96,7 +98,19 @@ public class PacketSequence
*/
public boolean isEncrypted()
{
return hasProprietaryDataHeader() && getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader;
if(hasProprietaryDataHeader())
{
if(getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader)
{
return true;
}
else if(getProprietaryDataHeader() instanceof HyteraDataEncryptionHeader hdeh)
{
return hdeh.getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION;
}
}
return false;
}
public int getTimeslot()

View File

@@ -25,17 +25,19 @@ import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
import io.github.dsheirer.module.decode.ip.DefinedShortDataPacket;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
@@ -167,19 +169,30 @@ public class PacketSequenceMessageFactory
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
else if(secondaryHeader instanceof HyteraProprietaryDataHeader)
else if(secondaryHeader instanceof HyteraDataEncryptionHeader hdeh)
{
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
ServiceAccessPoint sap = hdeh.getServiceAccessPoint();
if(hyteraTokenHeader.isSMSMessage())
switch(sap)
{
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
case SHORT_DATA:
return new DMRPacketMessage(packetSequence, new HyteraShortDataPacket(packetSequence, packet), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
case PROPRIETARY_DATA:
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
if(hyteraTokenHeader.isSMSMessage())
{
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
default:
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
else if(secondaryHeader instanceof MotorolaDataEncryptionHeader &&
@@ -231,8 +244,19 @@ public class PacketSequenceMessageFactory
*/
public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet)
{
return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
//Test to see if this is Hytera Radio Registration Service (RRS)
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
if(hyteraTokenHeader.isRRSMessage())
{
return new DMRPacketMessage(packetSequence, new HyteraRrsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
/**
@@ -248,7 +272,7 @@ public class PacketSequenceMessageFactory
if(sequence.hasProprietaryDataHeader())
{
CorrectedBinaryMessage prefix = ((ProprietaryDataHeader)sequence.getProprietaryDataHeader()).getPacketPrefix();
CorrectedBinaryMessage prefix = sequence.getProprietaryDataHeader().getPacketPrefix();
if(prefix != null)
{

View File

@@ -0,0 +1,82 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.type;
/**
* Enumeration of encryption algorithms
*/
public enum EncryptionAlgorithm
{
NO_ENCRYPTION(0x00, "NO ENCRYPTION"),
HYTERA_BASIC_PRIVACY(0x01, "HYTERA BP"),
HYTERA_ENHANCED_PRIVACY(0x02, "HYTERA RC4/EP"),
DMRA_RC4(0x21, "DMRA RC4/EP"),
DMRA_AES128(0x24, "DMRA AES128"),
DMRA_AES256(0x25, "DMRA AES256" ),
HYTERA_ENHANCED_PRIVACY_2(0x26, "HYTERA RC4/EP"),
UNKNOWN(-1, "UNKNOWN");
private final int mValue;
private final String mLabel;
/**
* Constructor
* @param value for the algorithm
* @param label to display
*/
EncryptionAlgorithm(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Numeric value for the algorithm
* @return value.
*/
public int getValue()
{
return mValue;
}
/**
* Utility method to lookup the encryption from the value.
* @param value to lookup
* @return matching entry or UNKNOWN
*/
public static EncryptionAlgorithm fromValue(int value)
{
for(EncryptionAlgorithm algorithm: EncryptionAlgorithm.values())
{
if(algorithm.getValue() == value)
{
return algorithm;
}
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -25,8 +25,8 @@ import io.github.dsheirer.edac.BPTC_16_2;
import io.github.dsheirer.edac.Golay24;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.Arc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NonStandardShortBurst;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NullShortBurst;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.ShortBurst;
@@ -38,8 +38,8 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.UnknownShortB
* Monitors audio call voice frame messaging to detect encrypted audio calls and extract the encryption parameters
* that are embedded into the six voice frames that comprise a voice super-frame.
* <p>
* See patent: https://patents.google.com/patent/EP2347540B1/en - embedding encryption parameters in voice super frame
* See patent: https://patents.google.com/patent/US8271009B2 - TXI - interrupting voice transmissions
* See patent: <a href="https://patents.google.com/patent/EP2347540B1/en">...</a> - embedding encryption parameters in voice super frame
* See patent: <a href="https://patents.google.com/patent/US8271009B2">...</a> - TXI - interrupting voice transmissions
*/
public class VoiceSuperFrameProcessor
{
@@ -47,8 +47,6 @@ public class VoiceSuperFrameProcessor
33, 34, 35, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] CRC4 = new int[]{56, 57, 58, 59};
private boolean mEncrypted = false;
private int mAlgorithm;
private int mKey;
private byte[] mFragmentA;
private byte[] mFragmentB;
private byte[] mFragmentC;
@@ -83,8 +81,6 @@ public class VoiceSuperFrameProcessor
private void softReset()
{
mAlgorithm = 0;
mKey = 0;
mFragmentA = null;
mFragmentB = null;
mFragmentC = null;
@@ -124,9 +120,6 @@ public class VoiceSuperFrameProcessor
{
if(voiceMessage instanceof VoiceEMBMessage voiceEMB)
{
boolean valid = voiceEMB.getEMB().isValid();
boolean encrypted = voiceEMB.getEMB().isEncrypted();
if(voiceEMB.getEMB().isValid() && voiceEMB.getEMB().isEncrypted())
{
mEncrypted = true;
@@ -175,7 +168,6 @@ public class VoiceSuperFrameProcessor
* initialization vector to return a parameters object.
*
* @param frameFFragment containing the short-burst FLC fragment
* @param iv that was previously extracted from the voice super-frame
* @return embedded parameters.
*/
private ShortBurst extractShortBurst(BinaryMessage frameFFragment)
@@ -191,7 +183,7 @@ public class VoiceSuperFrameProcessor
return switch(opcode)
{
case NULL -> new NullShortBurst(decoded);
case ARC4_ENCRYPTION -> new Arc4EncryptionParameters(decoded);
case ARC4_ENCRYPTION, AES128_ENCRYPTION, AES256_ENCRYPTION -> new EncryptionParameters(decoded);
case TXI_DELAY -> new TransmitInterrupt(decoded);
default -> new UnknownShortBurst(decoded);
};
@@ -233,11 +225,11 @@ public class VoiceSuperFrameProcessor
if(passes)
{
return String.format("%04X", iv);
return String.format("%08X", iv);
}
else
{
return String.format("%04X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
return String.format("%08X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
reassembled.getCorrectedBitCount() + ")";
}
}
@@ -262,10 +254,10 @@ public class VoiceSuperFrameProcessor
*/
public static boolean crc4(int value, int crc)
{
long checksum = (value & 0x0FFFFFFFFl) << 4;
long checksum = (value & 0x0FFFFFFFFL) << 4;
checksum ^= 0xF; //Initial fill
long polynomial = 0x013l << 31;
long checkBit = 0x1l << 35;
long polynomial = 0x013L << 31;
long checkBit = 0x1L << 35;
for(int x = 31; x >= 0; x--)
{

View File

@@ -20,11 +20,12 @@
package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
/**
* Encryption parameters short burst payload.
*/
public class Arc4EncryptionParameters extends ShortBurst
public class EncryptionParameters extends ShortBurst
{
private static final int[] KEY = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] ALGORITHM = new int[]{8, 9, 10};
@@ -34,7 +35,7 @@ public class Arc4EncryptionParameters extends ShortBurst
*
* @param message containing the de-interleaved and error-corrected short burst payload.
*/
public Arc4EncryptionParameters(CorrectedBinaryMessage message)
public EncryptionParameters(CorrectedBinaryMessage message)
{
super(message);
}
@@ -49,31 +50,45 @@ public class Arc4EncryptionParameters extends ShortBurst
}
/**
* Encryption algorithm
* @return algorithm (0 - 7). 1 = ARC4
* Encryption algorithm value
* @return algorithm (0 - 7).
*/
public int getAlgorithmValue()
{
return getMessage().getInt(ALGORITHM);
}
public String getAlgorithm()
/**
* Encryption algorithm.
* @return algorithm
*/
public EncryptionAlgorithm getAlgorithm()
{
int algorithm = getAlgorithmValue();
if(algorithm == 1)
return switch(algorithm)
{
return "EP/ARC4";
}
return "ALGORITHM:" + algorithm;
case 1 -> EncryptionAlgorithm.DMRA_RC4;
case 4 -> EncryptionAlgorithm.DMRA_AES128;
case 5 -> EncryptionAlgorithm.DMRA_AES256;
default -> EncryptionAlgorithm.UNKNOWN;
};
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("ENCRYPTION:").append(getAlgorithm());
sb.append("ENCRYPTION ALGORITHM:");
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
{
sb.append(getAlgorithmValue());
}
else
{
sb.append(getAlgorithm());
}
sb.append(" KEY:").append(getKey());
return sb.toString();
}

View File

@@ -21,17 +21,18 @@ package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
/**
* Short Burst opcode
*
* See: https://patents.google.com/patent/US8271009B2 Page 12
* See: <a href="https://patents.google.com/patent/US8271009B2">...</a> Page 12
*/
public enum ShortBurstOpcode
{
NULL(0),
ARC4_ENCRYPTION(1),
TXI_DELAY(3),
AES128_ENCRYPTION(4),
AES256_ENCRYPTION(5),
UNKNOWN(-1);
private int mValue;
private final int mValue;
/**
* Constructor

View File

@@ -221,7 +221,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
sb.append(",");
}
if(identifier.getForm() == Form.TALKGROUP)
if(identifier.getForm() == Form.TALKGROUP || identifier.getForm() == Form.RADIO || identifier.getForm() == Form.PATCH_GROUP)
{
sb.append(mUserPreferences.getTalkgroupFormatPreference().format(identifier));
}

View File

@@ -67,6 +67,7 @@ public enum DecodeEventType
PAGE("Page"),
QUERY("Query"),
RADIO_CHECK("Radio Check"),
RADIO_REGISTRATION_SERVICE("Hytera RRS"),
REGISTER("Register"),
REGISTER_ESN("ESN"),
REQUEST("Request"),
@@ -78,9 +79,10 @@ public enum DecodeEventType
STATUS("Status"),
UDP_PACKET("UDP/IP Packet"),
UNKNOWN_PACKET("Unknown Packet"),
XCMP("Motorola XCMP"),
UNKNOWN("Unknown");
private String mLabel;
private final String mLabel;
/**
* Encrypted voice call event types for filtering

View File

@@ -0,0 +1,130 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.hytera.rrs;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.DestinationId;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraToken;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenType;
import io.github.dsheirer.module.decode.p25.identifier.ipv4.DMRIpAddress;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera SDS Long Data Message with SMS encoded payload
*/
public class HyteraRrsPacket implements IPacket
{
private HyteraTokenHeader mHeader;
private Identifier mDestination;
private List<Identifier> mIdentifiers;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header to the packet within the message
*/
public HyteraRrsPacket(HyteraTokenHeader header)
{
mHeader = header;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("HYTERA RRS REGISTER");
if(hasDestination())
{
sb.append(" RADIO:").append(getDestination());
}
return sb.toString();
}
@Override
public HyteraTokenHeader getHeader()
{
return mHeader;
}
/**
* Destination radio identifier for the SMS message
*/
public Identifier getDestination()
{
if(mDestination == null)
{
HyteraToken destinationToken = getHeader().getTokenByType(HyteraTokenType.ID_DESTINATION);
if(destinationToken instanceof DestinationId destinationId)
{
if(((DestinationId) destinationToken).isIp())
{
mDestination = DMRIpAddress.createFrom(((DestinationId) destinationToken).getId());
}
else
{
mDestination = DMRRadio.createTo(destinationId.getId());
}
}
}
return mDestination;
}
/**
* Indicates if the SMS message has a destination identifier
*/
public boolean hasDestination()
{
return getDestination() != null;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
if(hasDestination())
{
mIdentifiers.add(getDestination());
}
}
return mIdentifiers;
}
@Override
public IPacket getPayload()
{
return null;
}
@Override
public boolean hasPayload()
{
return false;
}
}

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.module.decode.ip.hytera.sds;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.ipv4.IPV4Address;
/**
* Destination Identifier
@@ -45,10 +46,19 @@ public class DestinationId extends HyteraToken
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("DESTINATION:").append(getId());
sb.append("DESTINATION:").append(getIdFormatted());
return sb.toString();
}
/**
* Indicates if this is an IP address (true) or a Radio ID (false).
* @return true if IP
*/
public boolean isIp()
{
return getId() > 0x00FFFFFF;
}
/**
* Length of the content field in bytes.
* @return content field length.
@@ -69,6 +79,18 @@ public class DestinationId extends HyteraToken
return mMessage.getInt(ID);
}
public String getIdFormatted()
{
if(isIp())
{
return new IPV4Address(getId()).toString() + " [" + (getId() & 0xFFFFFF) + "]";
}
else
{
return String.valueOf(getId());
}
}
@Override
HyteraTokenType getTokenType()
{

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