mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 07:37:25 -05:00
Compare commits
98 Commits
v0.6.0-bet
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0492f19a95 | ||
|
|
098b039154 | ||
|
|
5c6e6059f8 | ||
|
|
72d45543df | ||
|
|
e96fc46315 | ||
|
|
f0cfbb80dd | ||
|
|
a199f03a60 | ||
|
|
526c9609ef | ||
|
|
ec9ceeefb3 | ||
|
|
b6c6505824 | ||
|
|
93c61c48f9 | ||
|
|
9b89e80bb5 | ||
|
|
e0ecaa6e08 | ||
|
|
a7c89b5d1a | ||
|
|
834846d339 | ||
|
|
45c8bb73fa | ||
|
|
d2580f8a99 | ||
|
|
804c2e6359 | ||
|
|
3f2ec612d5 | ||
|
|
8bead998f5 | ||
|
|
2ecdaba183 | ||
|
|
82cb10a03d | ||
|
|
8717ce38eb | ||
|
|
5858a85a50 | ||
|
|
65d16da997 | ||
|
|
b1b5ae5fd2 | ||
|
|
b5ec6d8877 | ||
|
|
8599aebed9 | ||
|
|
2b9e753033 | ||
|
|
dba50744a9 | ||
|
|
490569e023 | ||
|
|
e79f2af759 | ||
|
|
ec7ee1ce91 | ||
|
|
672207c27c | ||
|
|
a1a6b3e988 | ||
|
|
2347135614 | ||
|
|
93438ddd97 | ||
|
|
ae31c41643 | ||
|
|
16aed8741d | ||
|
|
e878ca0c7f | ||
|
|
ca61dd2543 | ||
|
|
6187253589 | ||
|
|
b51c80742f | ||
|
|
15352b9216 | ||
|
|
a848b21ae8 | ||
|
|
279aca7287 | ||
|
|
88879c956a | ||
|
|
729b7e27a8 | ||
|
|
feaed168ae | ||
|
|
50edb18db3 | ||
|
|
7302e2806d | ||
|
|
8845306b4a | ||
|
|
09e19e1486 | ||
|
|
0f5478925c | ||
|
|
09dce0b0d1 | ||
|
|
ad98b1cb5e | ||
|
|
8effd97749 | ||
|
|
9dbed72c32 | ||
|
|
b4dd3692b8 | ||
|
|
030cbb856c | ||
|
|
511101e9b7 | ||
|
|
60dd2b2df7 | ||
|
|
218dc94e02 | ||
|
|
2440f578b1 | ||
|
|
09c87237cb | ||
|
|
3d974ddd91 | ||
|
|
1489c970ba | ||
|
|
9e0afca3d1 | ||
|
|
99eda4013e | ||
|
|
a62f81f890 | ||
|
|
66f0731731 | ||
|
|
dc239801c6 | ||
|
|
d4ba0988bc | ||
|
|
ad0f953b2a | ||
|
|
e1bc9eabd6 | ||
|
|
d3dc1a8667 | ||
|
|
bf278120de | ||
|
|
2f52f6e009 | ||
|
|
02363c3113 | ||
|
|
cfaac0fcbc | ||
|
|
bee0405e00 | ||
|
|
50bdf0ee81 | ||
|
|
a27ddbe0d0 | ||
|
|
0d652b9153 | ||
|
|
9468fd189b | ||
|
|
215737ad9b | ||
|
|
123c25b142 | ||
|
|
af56198f0e | ||
|
|
3c73fdfd6e | ||
|
|
b391ebbb5f | ||
|
|
bad5a1f9f0 | ||
|
|
a7470b463f | ||
|
|
d6c706cb35 | ||
|
|
a0d6950c75 | ||
|
|
2495200b36 | ||
|
|
bfe7ae0b1b | ||
|
|
4912abbf07 | ||
|
|
2f2998d02e |
2
.github/workflows/gradle.yml
vendored
2
.github/workflows/gradle.yml
vendored
@@ -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
47
.github/workflows/nightly.yml
vendored
Normal 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
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,8 +1,10 @@
|
||||

|
||||

|
||||
|
||||
# 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
|
||||
|
||||
13
build.gradle
13
build.gradle
@@ -53,7 +53,8 @@ repositories {
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
version = '0.6.0-beta2'
|
||||
//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 {
|
||||
@@ -210,8 +211,8 @@ def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jd
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||
|
||||
/**
|
||||
@@ -228,7 +229,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||
//jdk.accessibility is used with assistive technologies like screen readers
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility')
|
||||
//java.management for JVM resource monitoring
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
@@ -339,7 +341,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||
}
|
||||
}
|
||||
//Linux x86-64
|
||||
//OSX x86-64
|
||||
if(file(jdk_osx_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||
}
|
||||
@@ -351,4 +353,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
|
||||
configure(rt, jvmArgsLinux);
|
||||
}
|
||||
|
||||
|
||||
31
gradle.properties
Normal file
31
gradle.properties
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# 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
|
||||
#
|
||||
# Example: 0.6.0-beta-5
|
||||
#
|
||||
# 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
|
||||
@@ -30,11 +30,13 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -77,6 +79,18 @@ public class AliasFactory
|
||||
Min copyMin = new Min();
|
||||
copyMin.setMin(originalMin.getMin());
|
||||
return copyMin;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
|
||||
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
|
||||
originalP25.getSystem(), originalP25.getValue());
|
||||
copyP25.setOverlap(originalP25.overlapProperty().get());
|
||||
return copyP25;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
|
||||
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
|
||||
originalFqt.getSystem(), originalFqt.getValue());
|
||||
copyFqt.setOverlap(originalFqt.overlapProperty().get());
|
||||
return copyFqt;
|
||||
case PRIORITY:
|
||||
Priority originalPriority = (Priority)id;
|
||||
Priority copyPriority = new Priority();
|
||||
@@ -184,6 +198,7 @@ public class AliasFactory
|
||||
copy.setGroup(original.getGroup());
|
||||
copy.setColor(original.getColor());
|
||||
copy.setIconName(original.getIconName());
|
||||
copy.setRecordable(original.isRecordable());
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -37,9 +39,11 @@ import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
@@ -155,6 +159,32 @@ public class AliasList
|
||||
|
||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio qualifiedRadio = (P25FullyQualifiedRadio) id;
|
||||
|
||||
RadioAliasList p25RadioAliasList = mRadioProtocolMap.get(qualifiedRadio.getProtocol());
|
||||
|
||||
if(p25RadioAliasList == null)
|
||||
{
|
||||
p25RadioAliasList = new RadioAliasList();
|
||||
mRadioProtocolMap.put(qualifiedRadio.getProtocol(), p25RadioAliasList);
|
||||
}
|
||||
|
||||
p25RadioAliasList.add(qualifiedRadio, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup qualifiedTalkgroup = (P25FullyQualifiedTalkgroup) id;
|
||||
|
||||
TalkgroupAliasList p25TalkgroupAliasList = mTalkgroupProtocolMap.get(qualifiedTalkgroup.getProtocol());
|
||||
|
||||
if(p25TalkgroupAliasList == null)
|
||||
{
|
||||
p25TalkgroupAliasList = new TalkgroupAliasList();
|
||||
mTalkgroupProtocolMap.put(qualifiedTalkgroup.getProtocol(), p25TalkgroupAliasList);
|
||||
}
|
||||
|
||||
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
|
||||
break;
|
||||
case RADIO_ID:
|
||||
Radio radio = (Radio)id;
|
||||
|
||||
@@ -388,6 +418,8 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case PATCH_GROUP:
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
@@ -395,8 +427,6 @@ public class AliasList
|
||||
|
||||
if(patchGroupAliasList != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
||||
|
||||
if(alias != null)
|
||||
@@ -404,19 +434,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;
|
||||
|
||||
@@ -427,7 +474,7 @@ public class AliasList
|
||||
return toList(radioAliasList.getAlias(radio));
|
||||
}
|
||||
break;
|
||||
case DCS:
|
||||
case ESN:
|
||||
if(identifier instanceof ESNIdentifier)
|
||||
{
|
||||
return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
|
||||
@@ -483,9 +530,7 @@ public class AliasList
|
||||
{
|
||||
if(alias != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
aliases.add(alias);
|
||||
return aliases;
|
||||
return Collections.singletonList(alias);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
@@ -608,6 +653,7 @@ public class AliasList
|
||||
*/
|
||||
public class TalkgroupAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -617,6 +663,11 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||
{
|
||||
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||
{
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
|
||||
}
|
||||
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||
@@ -638,26 +689,55 @@ public class AliasList
|
||||
|
||||
public void add(Talkgroup talkgroup, Alias alias)
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
fqt.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedTalkgroup existingFqt &&
|
||||
existingFqt.getHashKey().contentEquals(fqt.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedTalkgroupAliasMap.put(fqt.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
||||
@@ -690,6 +770,7 @@ public class AliasList
|
||||
*/
|
||||
public class RadioAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -699,6 +780,11 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(RadioIdentifier identifier)
|
||||
{
|
||||
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||
{
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
|
||||
}
|
||||
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mRadioAliasMap.get(value);
|
||||
@@ -720,26 +806,55 @@ public class AliasList
|
||||
|
||||
public void add(Radio radio, Alias alias)
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
|
||||
fqr.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
|
||||
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(RadioRange radioRange, Alias alias)
|
||||
|
||||
@@ -35,11 +35,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -61,6 +63,8 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||
|
||||
@@ -29,6 +29,8 @@ public enum AliasIDType
|
||||
LOJACK("LoJack"),
|
||||
LTR_NET_UID("LTR-Net UID"),
|
||||
MIN("Passport MIN"),
|
||||
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||
PRIORITY("Audio Priority"),
|
||||
RADIO_ID("Radio ID"),
|
||||
RADIO_ID_RANGE("Radio ID Range"),
|
||||
@@ -56,7 +58,7 @@ public enum AliasIDType
|
||||
|
||||
//Values used by the View-By alias editor
|
||||
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
||||
UNIT_STATUS, STATUS, TONES);
|
||||
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||
|
||||
public String toString()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.alias.id.radio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified radio. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedRadio extends Radio
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedRadio()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param id for the radio
|
||||
*/
|
||||
public P25FullyQualifiedRadio(int wacn, int system, int id)
|
||||
{
|
||||
super(Protocol.APCO25, id);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_RADIO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.alias.id.talkgroup;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified talkgroup. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedTalkgroup extends Talkgroup
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedTalkgroup()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param talkgroup value
|
||||
*/
|
||||
public P25FullyQualifiedTalkgroup(int wacn, int system, int talkgroup)
|
||||
{
|
||||
super(Protocol.APCO25, talkgroup);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_TALKGROUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||
|
||||
@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
@@ -67,6 +70,9 @@ public class BroadcastFactory
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
@@ -109,6 +115,8 @@ public class BroadcastFactory
|
||||
return new RdioScannerConfiguration(format);
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyFeedConfiguration(format);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzConfiguration(format);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPConfiguration(format);
|
||||
case ICECAST_TCP:
|
||||
|
||||
@@ -32,6 +32,7 @@ public enum BroadcastServerType
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
/**
|
||||
* HTTP headers used for posting to Rdio Scanner API
|
||||
*/
|
||||
public enum FormField
|
||||
{
|
||||
CALL("call"),
|
||||
FREQ("freq"),
|
||||
START_TIME("start_time"),
|
||||
STOP_TIME("stop_time"),
|
||||
CALL_LENGTH("call_length"),
|
||||
TALKGROUP_NUM("talkgroup_num"),
|
||||
EMERGENCY("emergency"),
|
||||
API_KEY("api_key"),
|
||||
SOURCE_LIST("source_list"),
|
||||
FREQ_LIST("freq_list"),
|
||||
PATCHES("patches");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
FormField(String header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
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;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
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;
|
||||
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.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;
|
||||
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
|
||||
*
|
||||
*/
|
||||
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
|
||||
|
||||
private static final String ENCODING_TYPE_MP3 = "mp3";
|
||||
private static final String MULTIPART_TYPE = "multipart";
|
||||
private static final String DEFAULT_SUBTYPE = "form-data";
|
||||
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
|
||||
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
|
||||
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
|
||||
private HttpClient mHttpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
/**
|
||||
* OpenMHz API does not currently expose a test method.
|
||||
* TODO: FIX THIS
|
||||
*/
|
||||
if(response == "OK")// && response.toLowerCase().startsWith("<head><title>502 bad gateway</title></head>"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
|
||||
if(mAudioRecordingProcessorFuture == null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
|
||||
0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioRecordingProcessorFuture != null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture.cancel(true);
|
||||
mAudioRecordingProcessorFuture = null;
|
||||
dispose();
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for disposal
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
audioRecording.removePendingReplay();
|
||||
audioRecording = mAudioRecordingQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
|
||||
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
|
||||
* the audio processor thread will persistently invoke this method to attempt a reconnect.
|
||||
*
|
||||
* OpenMHz does not have a test API endpoint, so we look for the incomplete call response.
|
||||
*/
|
||||
private boolean connected()
|
||||
{
|
||||
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response != null && response == "200")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioQueueSize()
|
||||
{
|
||||
return mAudioRecordingQueue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
mAudioRecordingQueue.offer(audioRecording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
|
||||
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
|
||||
* deleted to ensure that the in-memory queue size doesn't blow up.
|
||||
* @param audioRecording to test
|
||||
* @return true if the recording is valid
|
||||
*/
|
||||
private boolean isValid(AudioRecording audioRecording)
|
||||
{
|
||||
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
|
||||
getBroadcastConfiguration().getMaximumRecordingAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any enqueued audio recordings. This method employs asynchronous
|
||||
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
|
||||
*/
|
||||
private void processRecordingQueue()
|
||||
{
|
||||
|
||||
|
||||
while(connected() && !mAudioRecordingQueue.isEmpty())
|
||||
{
|
||||
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
|
||||
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
|
||||
{
|
||||
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||
String systemLabel = getSystemLabel(audioRecording);
|
||||
|
||||
try
|
||||
{
|
||||
byte[] audioBytes = null;
|
||||
|
||||
try
|
||||
{
|
||||
audioBytes = Files.readAllBytes(audioRecording.getPath());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
|
||||
}
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
|
||||
|
||||
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
|
||||
bodyBuilder
|
||||
.addFile(audioBytes)
|
||||
.addPart(FormField.FREQ, frequency)
|
||||
.addPart(FormField.START_TIME, timestampSeconds)
|
||||
.addPart(FormField.STOP_TIME, timestampSeconds)
|
||||
.addPart(FormField.CALL_LENGTH, durationSeconds)
|
||||
.addPart(FormField.TALKGROUP_NUM, talkgroup)
|
||||
.addPart(FormField.EMERGENCY, 0)
|
||||
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
// For future use if OpenMHz supports patch information
|
||||
//.addPart(FormField.PATCHES, patches)
|
||||
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((fileResponse, throwable1) -> {
|
||||
if(throwable1 != null || fileResponse.statusCode() != 200)
|
||||
{
|
||||
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
|
||||
{
|
||||
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||
//fully read our request and immediately responds.
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if(fileResponse.statusCode() == 200)
|
||||
{
|
||||
incrementStreamedAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Register an error for the file not found exception
|
||||
mLog.error("OpenMHz API - upload file not found [" +
|
||||
audioRecording.getPath().toString() + "]");
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unknown Error", e);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
|
||||
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
if(isValid(audioRecording))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
|
||||
mAudioRecordingQueue.poll();
|
||||
audioRecording.removePendingReplay();
|
||||
incrementAgedOffAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||
audioRecording = mAudioRecordingQueue.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a frequency value from the audio recording identifier collection.
|
||||
*/
|
||||
private static Long getFrequency(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||
Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||
|
||||
if(identifier instanceof ConfigurationLongIdentifier)
|
||||
{
|
||||
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return Long.valueOf(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
|
||||
*/
|
||||
private static String getFrom(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof RadioIdentifier)
|
||||
{
|
||||
return ((RadioIdentifier)identifier).getValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
|
||||
}
|
||||
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
|
||||
{
|
||||
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
|
||||
talkgroupIdentifier.getProtocol()));
|
||||
}
|
||||
else if(identifier instanceof RadioIdentifier radioIdentifier)
|
||||
{
|
||||
return radioIdentifier.getValue().toString();
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupLabel(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupGroup(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0).getGroup());
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the System Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
|
||||
*
|
||||
*/
|
||||
private String getSystemLabel(AudioRecording audioRecording)
|
||||
{
|
||||
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(!systems.isEmpty())
|
||||
{
|
||||
sb.append(systems.get(0));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the patched talkgroups
|
||||
*/
|
||||
public static String getPatches(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
return "[]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests both the connection and configuration against the OpenMHz Call API service
|
||||
* @param configuration containing API key and system id
|
||||
* @return error string or null if test is successful
|
||||
*/
|
||||
public static String testConnection(OpenMHzConfiguration configuration)
|
||||
{
|
||||
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
|
||||
.build();
|
||||
|
||||
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
String responseBody = response.body();
|
||||
if (response.statusCode() == 200)
|
||||
{
|
||||
return "OK";
|
||||
}
|
||||
else if(response.statusCode() == 403)
|
||||
{
|
||||
return "Invalid API Key";
|
||||
}
|
||||
else if(response.statusCode() == 500)
|
||||
{
|
||||
return "Invalid System Name";
|
||||
}
|
||||
|
||||
return "No Response";
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return e.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioRecordingProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
processRecordingQueue();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
OpenMHzConfiguration config = new OpenMHzConfiguration();
|
||||
config.setHost("https://api.OpenMHz.com/call-upload-dev");
|
||||
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
|
||||
config.setSystemName("systemx");
|
||||
|
||||
String response = testConnection(config);
|
||||
mLog.error("Response: " + response);
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
mLog.debug("Test Successful!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(response.contains("Invalid API Key"))
|
||||
{
|
||||
mLog.error("Invalid API Key");
|
||||
}
|
||||
else if(response.contains("Invalid System Name"))
|
||||
{
|
||||
mLog.error("Invalid System Name");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Response: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.lang.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Builder for an HTTP body publisher to produce a OpenMHz call event
|
||||
*/
|
||||
public class OpenMHzBuilder
|
||||
{
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
private byte[] audioBytes = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public OpenMHzBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the static multi-part boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio bytes part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addFile(byte[] value)
|
||||
{
|
||||
audioBytes = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, String value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, Number value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value.toString()));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form-data item
|
||||
*/
|
||||
private static String formatPart(Part part, String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
|
||||
sb.append(part.getValue()).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the audio file item
|
||||
*/
|
||||
private String formatFilePart(String boundary)
|
||||
{
|
||||
|
||||
if(audioBytes == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb= new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append("call").append("\"; filename=\"audio.mp3\"\r\n\r\n");
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the boundary closing item
|
||||
*/
|
||||
private static String getClosingBoundary(String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BodyPublisher for accessing the call form data
|
||||
*/
|
||||
public HttpRequest.BodyPublisher build()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(formatFilePart(BOUNDARY));
|
||||
|
||||
|
||||
/**
|
||||
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
|
||||
*/
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
outputStream.write(audioBytes);
|
||||
}
|
||||
|
||||
sb = new StringBuilder();
|
||||
sb.append("\r\n");
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
|
||||
}
|
||||
|
||||
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key:Value pair holder
|
||||
*/
|
||||
public class Part
|
||||
{
|
||||
private String mKey;
|
||||
private String mValue;
|
||||
|
||||
/**
|
||||
* Constructs a new part
|
||||
* @param key value
|
||||
* @param value item
|
||||
*/
|
||||
public Part(String key, String value)
|
||||
{
|
||||
mKey = key;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Streaming configuration for Rdio Scanner API.
|
||||
*
|
||||
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
|
||||
* with the structure of the audio streaming subsystem in sdrtrunk
|
||||
*/
|
||||
public class OpenMHzConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
public static final String PRODUCTION_ENDPOINT = "https://api.openmhz.com";
|
||||
|
||||
private StringProperty mSystemName = new SimpleStringProperty();
|
||||
private StringProperty mApiKey = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Constructor for faster jackson
|
||||
*/
|
||||
public OpenMHzConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
* @param format to use for audio recording (MP3)
|
||||
*/
|
||||
public OpenMHzConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
if(getHost() == null)
|
||||
{
|
||||
setHost(PRODUCTION_ENDPOINT);
|
||||
}
|
||||
|
||||
//The parent class binds this property, so we unbind it and rebind it here
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.and(Bindings.and(Bindings.isNotNull(mSystemName), Bindings.isNotNull(mApiKey)),
|
||||
Bindings.isNotNull(mHost)));
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as a property
|
||||
*/
|
||||
public StringProperty systemIDProperty()
|
||||
{
|
||||
return mSystemName;
|
||||
}
|
||||
|
||||
/**
|
||||
* API key as a property
|
||||
*/
|
||||
public StringProperty apiKeyProperty()
|
||||
{
|
||||
return mApiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
|
||||
public String getApiKey()
|
||||
{
|
||||
return mApiKey.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the api key
|
||||
* @param apiKey
|
||||
*/
|
||||
public void setApiKey(String apiKey)
|
||||
{
|
||||
mApiKey.setValue(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* System name as provided by OpenMHz.com
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system_name")
|
||||
public String getSystemName()
|
||||
{
|
||||
return mSystemName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system name provided by OpenMHz.com
|
||||
*/
|
||||
public void setSystemName(String systemName)
|
||||
{
|
||||
mSystemName.set(systemName);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzConfiguration copy = new OpenMHzConfiguration();
|
||||
copy.setSystemName(getSystemName());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
/**
|
||||
* OpenMHz calls API configuration editor
|
||||
*/
|
||||
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
|
||||
private TextField mSystemNameTextField;
|
||||
private IntegerTextField mMaxAgeTextField;
|
||||
private TextField mApiKeyTextField;
|
||||
private TextField mHostTextField;
|
||||
private GridPane mEditorPane;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for accessing the broadcast model
|
||||
*/
|
||||
public OpenMHzEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(OpenMHzConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getSystemNameTextField().setDisable(item == null);
|
||||
getApiKeyTextField().setDisable(item == null);
|
||||
getHostTextField().setDisable(item == null);
|
||||
getMaxAgeTextField().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getSystemNameTextField().setText(item.getSystemName());
|
||||
getApiKeyTextField().setText(item.getApiKey());
|
||||
getHostTextField().setText(item.getHost());
|
||||
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
|
||||
}
|
||||
else
|
||||
{
|
||||
getSystemNameTextField().setText(null);
|
||||
getApiKeyTextField().setText(null);
|
||||
getHostTextField().setText(null);
|
||||
getMaxAgeTextField().set(0);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystemName(getSystemNameTextField().getText());
|
||||
getItem().setHost(getHostTextField().getText());
|
||||
getItem().setApiKey(getApiKeyTextField().getText());
|
||||
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
|
||||
}
|
||||
|
||||
super.save();
|
||||
}
|
||||
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 5, 10,10));
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(5);
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label formatLabel = new Label("Format");
|
||||
GridPane.setHalignment(formatLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(formatLabel, 0, row);
|
||||
mEditorPane.getChildren().add(formatLabel);
|
||||
|
||||
GridPane.setConstraints(getFormatField(), 1, row);
|
||||
mEditorPane.getChildren().add(getFormatField());
|
||||
|
||||
Label enabledLabel = new Label("Enabled");
|
||||
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(enabledLabel, 2, row);
|
||||
mEditorPane.getChildren().add(enabledLabel);
|
||||
|
||||
GridPane.setConstraints(getEnabledSwitch(), 3, row);
|
||||
mEditorPane.getChildren().add(getEnabledSwitch());
|
||||
|
||||
Label systemLabel = new Label("Name");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getNameTextField());
|
||||
|
||||
Label apiKeyLabel = new Label("API Key");
|
||||
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(apiKeyLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(apiKeyLabel);
|
||||
|
||||
GridPane.setConstraints(getApiKeyTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getApiKeyTextField());
|
||||
|
||||
Label systemNameLabel = new Label("System Short Name");
|
||||
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemNameLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemNameLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getSystemNameTextField());
|
||||
|
||||
Label hostLabel = new Label("OpenMHz Upload Server (API)");
|
||||
GridPane.setHalignment(hostLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(hostLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(hostLabel);
|
||||
|
||||
GridPane.setConstraints(getHostTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getHostTextField());
|
||||
|
||||
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
|
||||
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(maxAgeLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(maxAgeLabel);
|
||||
|
||||
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getMaxAgeTextField());
|
||||
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private IntegerTextField getMaxAgeTextField()
|
||||
{
|
||||
if(mMaxAgeTextField == null)
|
||||
{
|
||||
mMaxAgeTextField = new IntegerTextField();
|
||||
mMaxAgeTextField.setDisable(true);
|
||||
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mMaxAgeTextField;
|
||||
}
|
||||
|
||||
private TextField getHostTextField()
|
||||
{
|
||||
if(mHostTextField == null)
|
||||
{
|
||||
mHostTextField = new TextField();
|
||||
mHostTextField.setDisable(true);
|
||||
mHostTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mHostTextField;
|
||||
}
|
||||
|
||||
private TextField getApiKeyTextField()
|
||||
{
|
||||
if(mApiKeyTextField == null)
|
||||
{
|
||||
mApiKeyTextField = new TextField();
|
||||
mApiKeyTextField.setDisable(true);
|
||||
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mApiKeyTextField;
|
||||
}
|
||||
|
||||
private TextField getSystemNameTextField()
|
||||
{
|
||||
if(mSystemNameTextField == null)
|
||||
{
|
||||
mSystemNameTextField = new TextField();
|
||||
mSystemNameTextField.setDisable(true);
|
||||
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mSystemNameTextField;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenMHzFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
|
||||
public OpenMHzFeedConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenMHz configuration for Icecast 2.3.2 compatible servers
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public OpenMHzFeedConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
setBitRate(16);
|
||||
setChannels(1);
|
||||
setSampleRate(8000);
|
||||
setInline(true);
|
||||
}
|
||||
|
||||
public static OpenMHzFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
OpenMHzFeedConfiguration config = new OpenMHzFeedConfiguration(BroadcastFormat.MP3);
|
||||
config.setName(userFeedBroadcast.getDescription());
|
||||
config.setHost(userFeedBroadcast.getHostname());
|
||||
config.setMountPoint(userFeedBroadcast.getMount());
|
||||
config.setFeedID(userFeedBroadcast.getFeedId());
|
||||
config.setPassword(userFeedBroadcast.getPassword());
|
||||
|
||||
try
|
||||
{
|
||||
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzFeedConfiguration copy = new OpenMHzFeedConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setInline(getInline());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setURL(getURL());
|
||||
|
||||
//OpenMHz Configuration Parameters
|
||||
copy.setFeedID(getFeedID());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
|
||||
public int getFeedID()
|
||||
{
|
||||
return mFeedID;
|
||||
}
|
||||
|
||||
public void setFeedID(int feedID)
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -278,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
@@ -525,7 +516,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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.BitSet;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
@@ -844,13 +845,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1273,7 +1279,7 @@ public class BinaryMessage extends BitSet
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
|
||||
}
|
||||
|
||||
return new String(bytes);
|
||||
return new String(bytes, Charset.forName("UTF-16"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,9 @@ import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
* Swing panel for Now Playing channels table and channel details tab set.
|
||||
*/
|
||||
public class NowPlayingPanel extends JPanel
|
||||
{
|
||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||
@@ -40,45 +43,95 @@ public class NowPlayingPanel extends JPanel
|
||||
private final DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel;
|
||||
private final ChannelPowerPanel mChannelPowerPanel;
|
||||
private JideTabbedPane mTabbedPane;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mDetailTabsVisible;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
* messages, events, and spectral view.
|
||||
*/
|
||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||
SettingsManager settingsManager)
|
||||
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||
{
|
||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||
mDetailTabsVisible = detailTabsVisible;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of the channel details tabs panel.
|
||||
* @param visible true to show or false to hide.
|
||||
*/
|
||||
public void setDetailTabsVisible(boolean visible)
|
||||
{
|
||||
//Only adjust if there is a change in state
|
||||
if(visible ^ mDetailTabsVisible)
|
||||
{
|
||||
mDetailTabsVisible = visible;
|
||||
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
else
|
||||
{
|
||||
getSplitPane().remove(getTabbedPane());
|
||||
}
|
||||
|
||||
revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private JideTabbedPane getTabbedPane()
|
||||
{
|
||||
if(mTabbedPane == null)
|
||||
{
|
||||
mTabbedPane = new JideTabbedPane();
|
||||
mTabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
mTabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
}
|
||||
|
||||
return mTabbedPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split pane for channels table and channel details tabs.
|
||||
*/
|
||||
private JideSplitPane getSplitPane()
|
||||
{
|
||||
if(mSplitPane == null)
|
||||
{
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
mSplitPane.setShowGripper(true);
|
||||
}
|
||||
|
||||
return mSplitPane;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
getSplitPane().add(mChannelMetadataPanel);
|
||||
|
||||
JideTabbedPane tabbedPane = new JideTabbedPane();
|
||||
tabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
tabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
tabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
tabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
tabbedPane.setFont(this.getFont());
|
||||
tabbedPane.setForeground(Color.BLACK);
|
||||
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
|
||||
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
splitPane.setShowGripper(true);
|
||||
splitPane.add(mChannelMetadataPanel);
|
||||
splitPane.add(tabbedPane);
|
||||
add(splitPane);
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
|
||||
add(getSplitPane());
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
|
||||
@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||
TunerManager tunerManager, UserPreferences userPreferences)
|
||||
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||
{
|
||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||
playlistManager.getAliasModel());
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
|
||||
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Now playing panel.
|
||||
*/
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
{
|
||||
return mNowPlayingPanel;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
||||
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
|
||||
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
|
||||
//Set preferred size to influence the split between these panels
|
||||
|
||||
@@ -365,7 +365,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
* @param request containing channel and other details
|
||||
* @throws ChannelException if a source is not available for the channel
|
||||
*/
|
||||
private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
|
||||
private synchronized void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
|
||||
{
|
||||
Channel channel = request.getChannel();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -110,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* 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.
|
||||
* 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/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
@@ -177,6 +180,28 @@ public class CRCDMR
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the residual CRC value using a mask of zero which should identify the mask value when an alternate
|
||||
* mask value is employed.
|
||||
* @param message to be checked
|
||||
* @param messageStart location
|
||||
* @param crcStart location
|
||||
* @return residual CRC check value.
|
||||
*/
|
||||
public static int calculateResidual(CorrectedBinaryMessage message, int messageStart, int crcStart)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
|
||||
}
|
||||
|
||||
int checksum = getIntChecksum(message, crcStart, 16);
|
||||
return calculated ^ checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
|
||||
* data blocks have a slightly complicated structure because the checksum
|
||||
|
||||
@@ -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,10 +35,14 @@ 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;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.monitor.StatusBox;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
@@ -67,7 +69,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 +84,15 @@ 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;
|
||||
private JFXPanel mStatusPanel;
|
||||
|
||||
/**
|
||||
* Constructs an instance. Note: this constructor is used for Swing applications.
|
||||
@@ -118,6 +121,27 @@ public class JavaFxWindowManager extends Application
|
||||
setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or accesses the JavaFX status panel, used by the main application GUI.
|
||||
* @param resourceMonitor for statistics
|
||||
* @return JFXPanel accessible on Swing thread that delegates JavaFX scene creation to the FX event thread.
|
||||
*/
|
||||
public JFXPanel getStatusPanel(ResourceMonitor resourceMonitor)
|
||||
{
|
||||
if(mStatusPanel == null)
|
||||
{
|
||||
mStatusPanel = new JFXPanel();
|
||||
|
||||
//JFXPanel has to be populated on the FX event thread
|
||||
Platform.runLater(() -> {
|
||||
Scene scene = new Scene(new StatusBox(resourceMonitor));
|
||||
mStatusPanel.setScene(scene);
|
||||
});
|
||||
}
|
||||
|
||||
return mStatusPanel;
|
||||
}
|
||||
|
||||
private void setup()
|
||||
{
|
||||
//Register this class to receive events via each method annotated with @Subscribe
|
||||
@@ -164,27 +188,7 @@ public class JavaFxWindowManager extends Application
|
||||
public void shutdown()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
|
||||
if(mChannelMapStage != null)
|
||||
{
|
||||
mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage);
|
||||
}
|
||||
|
||||
if(mIconManagerStage != null)
|
||||
{
|
||||
mUserPreferences.getJavaFxPreferences().unmonitor(mIconManagerStage);
|
||||
}
|
||||
|
||||
if(mPlaylistStage != null)
|
||||
{
|
||||
mUserPreferences.getJavaFxPreferences().unmonitor(mPlaylistStage);
|
||||
}
|
||||
|
||||
if(mUserPreferencesStage != null)
|
||||
{
|
||||
mUserPreferences.getJavaFxPreferences().unmonitor(mUserPreferencesStage);
|
||||
}
|
||||
|
||||
mUserPreferences.getJavaFxPreferences().clearStageMonitors();
|
||||
Platform.exit();
|
||||
}
|
||||
|
||||
@@ -195,31 +199,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()
|
||||
@@ -264,9 +268,7 @@ public class JavaFxWindowManager extends Application
|
||||
else
|
||||
{
|
||||
execute(() -> {
|
||||
getJmbeEditorStage().show();
|
||||
getJmbeEditorStage().requestFocus();
|
||||
getJmbeEditorStage().toFront();
|
||||
restoreStage(getJmbeEditorStage());
|
||||
getJmbeEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -337,9 +339,7 @@ public class JavaFxWindowManager extends Application
|
||||
execute(() -> {
|
||||
try
|
||||
{
|
||||
getPlaylistStage().show();
|
||||
getPlaylistStage().requestFocus();
|
||||
getPlaylistStage().toFront();
|
||||
restoreStage(getPlaylistStage());
|
||||
getPlaylistEditor().process(request);
|
||||
}
|
||||
catch(Throwable t)
|
||||
@@ -387,9 +387,7 @@ public class JavaFxWindowManager extends Application
|
||||
public void process(final ViewUserPreferenceEditorRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getUserPreferencesStage().show();
|
||||
getUserPreferencesStage().requestFocus();
|
||||
getUserPreferencesStage().toFront();
|
||||
restoreStage(getUserPreferencesStage());
|
||||
getUserPreferencesEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -428,11 +426,7 @@ public class JavaFxWindowManager extends Application
|
||||
@Subscribe
|
||||
public void process(final ViewIconManagerRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getIconManagerStage().show();
|
||||
getIconManagerStage().requestFocus();
|
||||
getIconManagerStage().toFront();
|
||||
});
|
||||
execute(() -> restoreStage(getIconManagerStage()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,9 +436,7 @@ public class JavaFxWindowManager extends Application
|
||||
public void process(final ViewChannelMapEditorRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getChannelMapStage().show();
|
||||
getChannelMapStage().requestFocus();
|
||||
getChannelMapStage().toFront();
|
||||
restoreStage(getChannelMapStage());
|
||||
getChannelMapEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -453,13 +445,24 @@ 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();
|
||||
});
|
||||
execute(() -> restoreStage(getRecordingViewerStage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the stage to previous size and location.
|
||||
* @param stage to restore.
|
||||
*/
|
||||
private void restoreStage(Stage stage)
|
||||
{
|
||||
stage.setIconified(false);
|
||||
stage.show();
|
||||
stage.requestFocus();
|
||||
stage.toFront();
|
||||
stage.setMinHeight(stage.getHeight());
|
||||
stage.setMinWidth(stage.getWidth());
|
||||
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,17 +32,18 @@ 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;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
@@ -54,7 +55,7 @@ import io.github.dsheirer.source.tuner.TunerEvent;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||
import io.github.dsheirer.spectrum.ClearTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
@@ -68,11 +69,11 @@ import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Point;
|
||||
import java.awt.Robot;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
@@ -80,7 +81,9 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
@@ -105,14 +108,19 @@ import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
public class SDRTrunk implements Listener<TunerEvent>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunk.class);
|
||||
|
||||
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
|
||||
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
|
||||
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
|
||||
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
|
||||
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
||||
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||
|
||||
private boolean mBroadcastStatusVisible;
|
||||
private boolean mResourceStatusVisible;
|
||||
private boolean mNowPlayingDetailsVisible;
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
@@ -127,6 +135,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
private UserPreferences mUserPreferences = new UserPreferences();
|
||||
private TunerManager mTunerManager;
|
||||
private ApplicationLog mApplicationLog;
|
||||
private ResourceMonitor mResourceMonitor;
|
||||
private JFXPanel mResourceStatusPanel;
|
||||
|
||||
private String mTitle;
|
||||
|
||||
@@ -140,6 +150,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||
mApplicationLog.start();
|
||||
|
||||
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||
@@ -155,18 +167,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
}
|
||||
}
|
||||
|
||||
//Setup the application home directory
|
||||
Path home = getHomePath();
|
||||
|
||||
ThreadPool.logSettings();
|
||||
|
||||
mLog.info("Home path: " + home.toString());
|
||||
|
||||
//Load properties file
|
||||
if(home != null)
|
||||
{
|
||||
loadProperties(home);
|
||||
}
|
||||
loadProperties();
|
||||
|
||||
//Log current properties setting
|
||||
SystemProperties.getInstance().logCurrentSettings();
|
||||
@@ -213,10 +217,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
MapService mapService = new MapService(mIconModel);
|
||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||
|
||||
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
||||
mSettingsManager, mTunerManager, mUserPreferences);
|
||||
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||
@@ -323,7 +329,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
*/
|
||||
private void initGUI()
|
||||
{
|
||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
|
||||
|
||||
/**
|
||||
* Setup main JFrame window
|
||||
@@ -379,7 +385,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mSplitPane.add(mSpectralPanel);
|
||||
mSplitPane.add(mControllerPanel);
|
||||
|
||||
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
|
||||
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
|
||||
|
||||
//Show broadcast status panel when user requests - disabled by default
|
||||
if(mBroadcastStatusVisible)
|
||||
@@ -389,6 +395,13 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
||||
|
||||
mResourceMonitor.start();
|
||||
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu items
|
||||
*/
|
||||
@@ -399,14 +412,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||
exitMenu.addActionListener(
|
||||
new ActionListener()
|
||||
{
|
||||
public void actionPerformed(ActionEvent event)
|
||||
{
|
||||
processShutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
exitMenu.addActionListener(event -> {
|
||||
processShutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -415,56 +423,121 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
JMenu viewMenu = new JMenu("View");
|
||||
|
||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||
viewMenu.add(viewPlaylistItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
|
||||
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
|
||||
viewMenu.add(dmrMessageViewerMenu);
|
||||
|
||||
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
|
||||
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(settingsMenu);
|
||||
|
||||
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
|
||||
logFilesMenu.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0)
|
||||
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(getHomePath().toFile());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
Desktop.getDesktop().open(logsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
getHomePath().toString(),
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + logsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(logFilesMenu);
|
||||
viewMenu.add(viewApplicationLogsMenu);
|
||||
|
||||
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
|
||||
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewRecordingsMenuItem.addActionListener(arg0 -> {
|
||||
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
|
||||
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(recordingsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
recordingsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewRecordingsMenuItem);
|
||||
|
||||
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
|
||||
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewEventLogsMenu.addActionListener(arg0 -> {
|
||||
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(eventLogsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + eventLogsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewEventLogsMenu);
|
||||
|
||||
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
|
||||
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
|
||||
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(iconManagerMenu);
|
||||
|
||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
|
||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||
viewMenu.add(recordingViewerMenu);
|
||||
|
||||
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
|
||||
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewScreenCapturesMenu.addActionListener(arg0 -> {
|
||||
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(screenCapturesDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + screenCapturesDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewScreenCapturesMenu);
|
||||
|
||||
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||
viewMenu.add(preferencesItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new TunersMenu());
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
|
||||
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
|
||||
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||
@@ -515,6 +588,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mLog.info("Stopping channels ...");
|
||||
mPlaylistManager.getChannelProcessingManager().shutdown();
|
||||
mAudioRecordingManager.stop();
|
||||
mResourceMonitor.stop();
|
||||
|
||||
mLog.info("Stopping spectral display ...");
|
||||
mSpectralPanel.clearTuner();
|
||||
@@ -560,7 +634,54 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for resource status panel
|
||||
*/
|
||||
private JFXPanel getResourceStatusPanel()
|
||||
{
|
||||
|
||||
if(mResourceStatusPanel == null)
|
||||
{
|
||||
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
|
||||
}
|
||||
|
||||
return mResourceStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the resource status panel at the bottom of the main UI window
|
||||
*/
|
||||
private void toggleResourceStatusPanelVisibility()
|
||||
{
|
||||
mResourceStatusVisible = !mResourceStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.remove(getResourceStatusPanel());
|
||||
}
|
||||
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the Now Playing channel details panel
|
||||
*/
|
||||
private void toggleNowPlayingDetailsPanelVisibility()
|
||||
{
|
||||
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
|
||||
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
|
||||
@@ -568,34 +689,30 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
* Loads the application properties file from the user's home directory,
|
||||
* creating the properties file for the first-time, if necessary
|
||||
*/
|
||||
private void loadProperties(Path homePath)
|
||||
private void loadProperties()
|
||||
{
|
||||
Path propsPath = homePath.resolve("SDRTrunk.properties");
|
||||
Path propertiesPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot().resolve("SDRTrunk.properties");
|
||||
|
||||
if(!Files.exists(propsPath))
|
||||
if(!Files.exists(propertiesPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
mLog.info("SDRTrunk - creating application properties file [" +
|
||||
propsPath.toAbsolutePath() + "]");
|
||||
|
||||
Files.createFile(propsPath);
|
||||
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
|
||||
Files.createFile(propertiesPath);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't create application properties "
|
||||
+ "file [" + propsPath.toAbsolutePath(), e);
|
||||
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if(Files.exists(propsPath))
|
||||
if(Files.exists(propertiesPath))
|
||||
{
|
||||
SystemProperties.getInstance().load(propsPath);
|
||||
SystemProperties.getInstance().load(propertiesPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
|
||||
"application properties file");
|
||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,26 +795,50 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast status panel visible toggle menu item
|
||||
*/
|
||||
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
private ControllerPanel mControllerPanel;
|
||||
|
||||
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
|
||||
public BroadcastStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Streaming Status");
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mControllerPanel = controllerPanel;
|
||||
/**
|
||||
* Resource status panel visible toggle menu item
|
||||
*/
|
||||
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public ResourceStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Resource Status");
|
||||
setSelected(mResourceStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleResourceStatusPanelVisibility();
|
||||
setSelected(mResourceStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(mBroadcastStatusPanel != null);
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
}
|
||||
/**
|
||||
* Now Playing channel details visible toggle menu item
|
||||
*/
|
||||
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public NowPlayingChannelDetailsVisibleMenuItem()
|
||||
{
|
||||
super("Show Now Playing Channel Details");
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
addActionListener(e -> {
|
||||
toggleNowPlayingDetailsPanelVisibility();
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2021 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
|
||||
@@ -19,16 +19,12 @@
|
||||
|
||||
package io.github.dsheirer.gui.playlist.alias;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
import com.google.common.collect.Ordering;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import java.util.List;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -49,6 +45,7 @@ import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Callback;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
/**
|
||||
* Editor for multiple selected aliases providing limited options for changing attributes of multiple aliases
|
||||
@@ -65,6 +62,8 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
private ToggleSwitch mMonitorAudioToggleSwitch;
|
||||
private ComboBox<Integer> mMonitorPriorityComboBox;
|
||||
private Button mApplyMonitorButton;
|
||||
private ToggleSwitch mRecordToggleSwitch;
|
||||
private Button mApplyRecordButton;
|
||||
|
||||
private BooleanProperty mChangeInProgressProperty;
|
||||
private ReadOnlyBooleanProperty mChangeInProgressROProperty;
|
||||
@@ -144,6 +143,17 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
|
||||
gridPane.getChildren().add(getApplyMonitorButton());
|
||||
|
||||
Label recordLabel = new Label("Record");
|
||||
GridPane.setHalignment(recordLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(recordLabel, 0, ++row);
|
||||
gridPane.getChildren().add(recordLabel);
|
||||
|
||||
GridPane.setConstraints(getRecordToggleSwitch(), 1, row);
|
||||
gridPane.getChildren().add(getRecordToggleSwitch());
|
||||
|
||||
GridPane.setConstraints(getApplyRecordButton(), 4, row);
|
||||
gridPane.getChildren().add(getApplyRecordButton());
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
@@ -326,6 +336,19 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
return mMonitorAudioToggleSwitch;
|
||||
}
|
||||
|
||||
private ToggleSwitch getRecordToggleSwitch()
|
||||
{
|
||||
if(mRecordToggleSwitch == null)
|
||||
{
|
||||
mRecordToggleSwitch = new ToggleSwitch();
|
||||
mRecordToggleSwitch.setSelected(false);
|
||||
mRecordToggleSwitch.selectedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mRecordToggleSwitch;
|
||||
}
|
||||
|
||||
private ComboBox<Integer> getMonitorPriorityComboBox()
|
||||
{
|
||||
if(mMonitorPriorityComboBox == null)
|
||||
@@ -346,8 +369,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
return mMonitorPriorityComboBox;
|
||||
}
|
||||
|
||||
private static int setCount = 0;
|
||||
|
||||
private Button getApplyMonitorButton()
|
||||
{
|
||||
if(mApplyMonitorButton == null)
|
||||
@@ -378,7 +399,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
for(Alias alias : getItem())
|
||||
{
|
||||
alias.setCallPriority(pri);
|
||||
//System.out.println(++setCount);
|
||||
}
|
||||
|
||||
endChange();
|
||||
@@ -389,6 +409,25 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
return mApplyMonitorButton;
|
||||
}
|
||||
|
||||
private Button getApplyRecordButton()
|
||||
{
|
||||
if(mApplyRecordButton == null)
|
||||
{
|
||||
mApplyRecordButton = new Button("Apply");
|
||||
mApplyRecordButton.setOnAction(event -> {
|
||||
startChange();
|
||||
boolean record = getRecordToggleSwitch().isSelected();
|
||||
for(Alias alias : getItem())
|
||||
{
|
||||
alias.setRecordable(record);
|
||||
}
|
||||
endChange();
|
||||
});
|
||||
}
|
||||
|
||||
return mApplyRecordButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell factory for combo box for dislaying icon name and graphic
|
||||
*/
|
||||
|
||||
@@ -387,7 +387,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
idsColumn.setCellValueFactory(new IdentifierCountCell());
|
||||
|
||||
TableColumn<Alias, Boolean> errorsColumn = new TableColumn<>("Error");
|
||||
errorsColumn.setPrefWidth(80);
|
||||
errorsColumn.setPrefWidth(120);
|
||||
errorsColumn.setCellValueFactory(new PropertyValueFactory<>("overlap"));
|
||||
errorsColumn.setCellFactory(param ->
|
||||
{
|
||||
|
||||
@@ -37,11 +37,13 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
@@ -362,7 +364,10 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
for(AliasID aliasID: getIdentifiersList().getItems())
|
||||
{
|
||||
//Create a copy of the identifier so that the alias and the editor don't have the same instance
|
||||
alias.addAliasID(AliasFactory.copyOf(aliasID));
|
||||
//and reset the overlap flag (if set) so that the alias list can reevaluate the overlap state.
|
||||
AliasID copy = AliasFactory.copyOf(aliasID);
|
||||
copy.setOverlap(false);
|
||||
alias.addAliasID(copy);
|
||||
}
|
||||
|
||||
//Remove and replace alias actions
|
||||
@@ -681,8 +686,10 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
||||
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddP25FullyQualifiedTalkgroupItem());
|
||||
p25Menu.getItems().add(new AddRadioIdItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddP25FullyQualifiedRadioIdItem());
|
||||
p25Menu.getItems().add(new SeparatorMenuItem());
|
||||
p25Menu.getItems().add(new AddUserStatusItem());
|
||||
p25Menu.getItems().add(new AddUnitStatusItem());
|
||||
@@ -1319,6 +1326,44 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Fully Qualified Radio ID alias identifier
|
||||
*/
|
||||
public class AddP25FullyQualifiedRadioIdItem extends MenuItem
|
||||
{
|
||||
public AddP25FullyQualifiedRadioIdItem()
|
||||
{
|
||||
super("Fully Qualified Radio ID");
|
||||
setOnAction(event -> {
|
||||
P25FullyQualifiedRadio radioId = new P25FullyQualifiedRadio();
|
||||
radioId.setProtocol(Protocol.APCO25);
|
||||
getIdentifiersList().getItems().add(radioId);
|
||||
getIdentifiersList().getSelectionModel().select(radioId);
|
||||
getIdentifiersList().scrollTo(radioId);
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Fully Qualified Talkgroup alias identifier
|
||||
*/
|
||||
public class AddP25FullyQualifiedTalkgroupItem extends MenuItem
|
||||
{
|
||||
public AddP25FullyQualifiedTalkgroupItem()
|
||||
{
|
||||
super("Fully Qualified Talkgroup");
|
||||
setOnAction(event -> {
|
||||
P25FullyQualifiedTalkgroup talkgroup = new P25FullyQualifiedTalkgroup();
|
||||
talkgroup.setProtocol(Protocol.APCO25);
|
||||
getIdentifiersList().getItems().add(talkgroup);
|
||||
getIdentifiersList().getSelectionModel().select(talkgroup);
|
||||
getIdentifiersList().scrollTo(talkgroup);
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
|
||||
*/
|
||||
@@ -1502,7 +1547,25 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
if(item instanceof Talkgroup)
|
||||
if(item instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("APCO-25 Fully Qualified Talkgroup:").append(fqt.getWacn());
|
||||
sb.append(".").append(fqt.getSystem());
|
||||
sb.append(".").append(fqt.getValue());
|
||||
|
||||
if(fqt.overlapProperty().get())
|
||||
{
|
||||
sb.append(" - Error: Overlap");
|
||||
}
|
||||
|
||||
if(!fqt.isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof Talkgroup)
|
||||
{
|
||||
Talkgroup talkgroup = (Talkgroup)item;
|
||||
Protocol protocol = talkgroup.getProtocol();
|
||||
@@ -1549,6 +1612,24 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("APCO-25 Fully Qualified Radio ID:").append(fqr.getWacn());
|
||||
sb.append(".").append(fqr.getSystem());
|
||||
sb.append(".").append(fqr.getValue());
|
||||
|
||||
if(fqr.overlapProperty().get())
|
||||
{
|
||||
sb.append(" - Error: Overlap");
|
||||
}
|
||||
|
||||
if(!fqr.isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof Radio)
|
||||
{
|
||||
Radio radio = (Radio)item;
|
||||
|
||||
@@ -41,6 +41,10 @@ public class IdentifierEditorFactory
|
||||
return new EsnEditor();
|
||||
case LOJACK:
|
||||
return new LojackEditor();
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
return new P25FullyQualifiedRadioIdEditor(userPreferences);
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
return new P25FullyQualifiedTalkgroupEditor(userPreferences);
|
||||
case RADIO_ID:
|
||||
return new RadioIdEditor(userPreferences);
|
||||
case RADIO_ID_RANGE:
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.alias.identifier;
|
||||
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.gui.control.HexFormatter;
|
||||
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Editor for P25 fully qualified radio alias identifiers
|
||||
*/
|
||||
public class P25FullyQualifiedRadioIdEditor extends IdentifierEditor<P25FullyQualifiedRadio>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedRadioIdEditor.class);
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private Label mProtocolLabel;
|
||||
private TextField mWacnField;
|
||||
private TextField mSystemField;
|
||||
private TextField mRadioField;
|
||||
private TextFormatter<Integer> mWacnTextFormatter;
|
||||
private TextFormatter<Integer> mSystemTextFormatter;
|
||||
private TextFormatter<Integer> mRadioTextFormatter;
|
||||
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||
private RadioValueChangeListener mRadioValueChangeListener = new RadioValueChangeListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param userPreferences for determining user preferred talkgroup formats
|
||||
*/
|
||||
public P25FullyQualifiedRadioIdEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(3);
|
||||
|
||||
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||
gridPane.getChildren().add(getProtocolLabel());
|
||||
|
||||
Label valueLabel = new Label("WACN");
|
||||
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(valueLabel, 1, 0);
|
||||
gridPane.getChildren().add(valueLabel);
|
||||
|
||||
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||
gridPane.getChildren().add(getWacnField());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 3, 0);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||
gridPane.getChildren().add(getSystemField());
|
||||
|
||||
Label radioLabel = new Label("Radio ID");
|
||||
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(radioLabel, 5, 0);
|
||||
gridPane.getChildren().add(radioLabel);
|
||||
|
||||
GridPane.setConstraints(getRadioField(), 6, 0);
|
||||
gridPane.getChildren().add(getRadioField());
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(P25FullyQualifiedRadio item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
P25FullyQualifiedRadio fqr = getItem();
|
||||
|
||||
getProtocolLabel().setDisable(fqr == null);
|
||||
getWacnField().setDisable(fqr == null);
|
||||
getSystemField().setDisable(fqr == null);
|
||||
getRadioField().setDisable(fqr == null);
|
||||
|
||||
if(fqr != null)
|
||||
{
|
||||
getProtocolLabel().setText(fqr.getProtocol().toString());
|
||||
updateTextFormatter();
|
||||
}
|
||||
else
|
||||
{
|
||||
getWacnField().setText(null);
|
||||
getSystemField().setText(null);
|
||||
getRadioField().setText(null);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
private void updateTextFormatter()
|
||||
{
|
||||
if(mWacnTextFormatter != null)
|
||||
{
|
||||
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||
}
|
||||
if(mSystemTextFormatter != null)
|
||||
{
|
||||
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||
}
|
||||
if(mRadioTextFormatter != null)
|
||||
{
|
||||
mRadioTextFormatter.valueProperty().removeListener(mRadioValueChangeListener);
|
||||
}
|
||||
|
||||
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||
|
||||
if(format == IntegerFormat.DECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof IntegerFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||
mRadioTextFormatter = new IntegerFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||
mRadioField.setTooltip(new Tooltip("Format: 0 - 16777215"));
|
||||
}
|
||||
else if(format == IntegerFormat.HEXADECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof HexFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||
mRadioTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||
mRadioField.setTooltip(new Tooltip("Format: 0 - FFFFFF"));
|
||||
}
|
||||
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||
|
||||
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||
mRadioTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||
|
||||
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||
mRadioTextFormatter.valueProperty().addListener(mRadioValueChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
private Label getProtocolLabel()
|
||||
{
|
||||
if(mProtocolLabel == null)
|
||||
{
|
||||
mProtocolLabel = new Label();
|
||||
}
|
||||
|
||||
return mProtocolLabel;
|
||||
}
|
||||
|
||||
private TextField getWacnField()
|
||||
{
|
||||
if(mWacnField == null)
|
||||
{
|
||||
mWacnField = new TextField();
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
}
|
||||
|
||||
return mWacnField;
|
||||
}
|
||||
|
||||
private TextField getSystemField()
|
||||
{
|
||||
if(mSystemField == null)
|
||||
{
|
||||
mSystemField = new TextField();
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
}
|
||||
|
||||
return mSystemField;
|
||||
}
|
||||
|
||||
private TextField getRadioField()
|
||||
{
|
||||
if(mRadioField == null)
|
||||
{
|
||||
mRadioField = new TextField();
|
||||
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||
}
|
||||
|
||||
return mRadioField;
|
||||
}
|
||||
|
||||
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setWacn(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystem(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RadioValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setValue(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.alias.identifier;
|
||||
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.gui.control.HexFormatter;
|
||||
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Editor for P25 fully qualified talkgroup alias identifiers
|
||||
*/
|
||||
public class P25FullyQualifiedTalkgroupEditor extends IdentifierEditor<P25FullyQualifiedTalkgroup>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedTalkgroupEditor.class);
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private Label mProtocolLabel;
|
||||
private TextField mWacnField;
|
||||
private TextField mSystemField;
|
||||
private TextField mTalkgroupField;
|
||||
private TextFormatter<Integer> mWacnTextFormatter;
|
||||
private TextFormatter<Integer> mSystemTextFormatter;
|
||||
private TextFormatter<Integer> mTalkgroupTextFormatter;
|
||||
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||
private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param userPreferences for determining user preferred talkgroup formats
|
||||
*/
|
||||
public P25FullyQualifiedTalkgroupEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(3);
|
||||
|
||||
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||
gridPane.getChildren().add(getProtocolLabel());
|
||||
|
||||
Label valueLabel = new Label("WACN");
|
||||
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(valueLabel, 1, 0);
|
||||
gridPane.getChildren().add(valueLabel);
|
||||
|
||||
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||
gridPane.getChildren().add(getWacnField());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 3, 0);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||
gridPane.getChildren().add(getSystemField());
|
||||
|
||||
Label radioLabel = new Label("Talkgroup");
|
||||
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(radioLabel, 5, 0);
|
||||
gridPane.getChildren().add(radioLabel);
|
||||
|
||||
GridPane.setConstraints(getTalkgroupField(), 6, 0);
|
||||
gridPane.getChildren().add(getTalkgroupField());
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(P25FullyQualifiedTalkgroup item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
P25FullyQualifiedTalkgroup fqt = getItem();
|
||||
|
||||
getProtocolLabel().setDisable(fqt == null);
|
||||
getWacnField().setDisable(fqt == null);
|
||||
getSystemField().setDisable(fqt == null);
|
||||
getTalkgroupField().setDisable(fqt == null);
|
||||
|
||||
if(fqt != null)
|
||||
{
|
||||
getProtocolLabel().setText(fqt.getProtocol().toString());
|
||||
updateTextFormatter();
|
||||
}
|
||||
else
|
||||
{
|
||||
getWacnField().setText(null);
|
||||
getSystemField().setText(null);
|
||||
getTalkgroupField().setText(null);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
private void updateTextFormatter()
|
||||
{
|
||||
if(mWacnTextFormatter != null)
|
||||
{
|
||||
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||
}
|
||||
if(mSystemTextFormatter != null)
|
||||
{
|
||||
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||
}
|
||||
if(mTalkgroupTextFormatter != null)
|
||||
{
|
||||
mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
|
||||
}
|
||||
|
||||
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||
|
||||
if(format == IntegerFormat.DECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof IntegerFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||
mTalkgroupTextFormatter = new IntegerFormatter(0,0xFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - 65535"));
|
||||
}
|
||||
else if(format == IntegerFormat.HEXADECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof HexFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||
mTalkgroupTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - FFFF"));
|
||||
}
|
||||
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||
|
||||
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||
mTalkgroupTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||
|
||||
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||
mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
private Label getProtocolLabel()
|
||||
{
|
||||
if(mProtocolLabel == null)
|
||||
{
|
||||
mProtocolLabel = new Label();
|
||||
}
|
||||
|
||||
return mProtocolLabel;
|
||||
}
|
||||
|
||||
private TextField getWacnField()
|
||||
{
|
||||
if(mWacnField == null)
|
||||
{
|
||||
mWacnField = new TextField();
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
}
|
||||
|
||||
return mWacnField;
|
||||
}
|
||||
|
||||
private TextField getSystemField()
|
||||
{
|
||||
if(mSystemField == null)
|
||||
{
|
||||
mSystemField = new TextField();
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
}
|
||||
|
||||
return mSystemField;
|
||||
}
|
||||
|
||||
private TextField getTalkgroupField()
|
||||
{
|
||||
if(mTalkgroupField == null)
|
||||
{
|
||||
mTalkgroupField = new TextField();
|
||||
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||
}
|
||||
|
||||
return mTalkgroupField;
|
||||
}
|
||||
|
||||
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setWacn(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystem(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TalkgroupValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setValue(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.AgencyInfo;
|
||||
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||
import io.github.dsheirer.service.radioreference.RadioReference;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.application.Platform;
|
||||
@@ -158,8 +159,13 @@ public class AgencyEditor extends VBox
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
try
|
||||
{
|
||||
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||
if (mLevel == Level.COUNTY && agency instanceof CountyAgency) {
|
||||
final CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(-agency.getAgencyId());
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(countyInfo.getCategories()));
|
||||
} else {
|
||||
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
@@ -196,11 +202,11 @@ public class AgencyEditor extends VBox
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if(o1.getName() == null)
|
||||
else if(o1.getName() == null || o2 instanceof CountyAgency)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if(o2.getName() == null)
|
||||
else if(o2.getName() == null || o1 instanceof CountyAgency)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -158,9 +158,12 @@ public class AgencyFrequencyEditor extends GridPane
|
||||
|
||||
if(categories != null && !categories.isEmpty())
|
||||
{
|
||||
getCategoryComboBox().getItems().add(ALL_CATEGORIES);
|
||||
// Only allow the combined list of all categories if there aren't that many
|
||||
if (categories.size() < 10) {
|
||||
categories.add(0, ALL_CATEGORIES);
|
||||
}
|
||||
getCategoryComboBox().getItems().addAll(categories);
|
||||
getCategoryComboBox().getSelectionModel().select(ALL_CATEGORIES);
|
||||
getCategoryComboBox().getSelectionModel().select(categories.get(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.radioreference;
|
||||
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||
|
||||
public class CountyAgency extends Agency
|
||||
{
|
||||
public CountyAgency(CountyInfo countyInfo)
|
||||
{
|
||||
this.setAgencyId(-countyInfo.getCountyId());
|
||||
this.setName(countyInfo.getName() + " County (All)");
|
||||
this.setType(-1);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.radioreference;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.RadioReferenceException;
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.AuthorizationInformation;
|
||||
import io.github.dsheirer.rrapi.type.Country;
|
||||
import io.github.dsheirer.rrapi.type.CountryInfo;
|
||||
@@ -55,6 +56,7 @@ import jiconfont.javafx.IconNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -689,7 +691,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer<Authori
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getCountySystemEditor().setSystems(countyInfo.getSystems());
|
||||
getCountyAgencyEditor().setAgencies(countyInfo.getAgencies());
|
||||
|
||||
// Make a new list so we don't add our CountyAgency to the original list of agencies
|
||||
List<Agency> countyAgencies = new ArrayList<Agency>();
|
||||
countyAgencies.add(new CountyAgency(countyInfo));
|
||||
countyAgencies.addAll(countyInfo.getAgencies());
|
||||
getCountyAgencyEditor().setAgencies(countyAgencies);
|
||||
});
|
||||
}
|
||||
catch(RadioReferenceException rre)
|
||||
|
||||
@@ -294,7 +294,11 @@ public class SystemEditor extends VBox
|
||||
{
|
||||
CountyInfo countyInfo = null;
|
||||
|
||||
if(site.getCountyId() > 0)
|
||||
int countyId = site.getCountyId();
|
||||
|
||||
//Temporary sites that have been added to a system where the county/location is unknown, can
|
||||
//have a county ID of 99,999 as observed from API (undocumented).
|
||||
if(countyId > 0 && countyId != 99999)
|
||||
{
|
||||
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class StreamEditorFactory
|
||||
return new BroadcastifyStreamEditor(playlistManager);
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerEditor(playlistManager);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzEditor(playlistManager);
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallEditor(playlistManager);
|
||||
case ICECAST_HTTP:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
||||
import java.io.File;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -31,6 +32,7 @@ import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -39,8 +41,6 @@ import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
/**
|
||||
* Preference settings for channel event view
|
||||
@@ -97,6 +97,9 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
private Button mResetStreamingButton;
|
||||
private Label mStreamingPathLabel;
|
||||
|
||||
private Spinner<Integer> mRecordingSpinner;
|
||||
private Spinner<Integer> mEventLogSpinner;
|
||||
|
||||
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
||||
@@ -247,11 +250,57 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
|
||||
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
||||
mEditorPane.add(getResetStreamingButton(), 3, row++);
|
||||
|
||||
Label monitorLabel = new Label("File storage usage monitoring - maximum size thresholds (MB)");
|
||||
GridPane.setMargin(monitorLabel, new Insets(15, 0, 2, 0));
|
||||
mEditorPane.add(monitorLabel, 0, row++, 4, 1);
|
||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, row++, 4, 1);
|
||||
|
||||
mEditorPane.add(new Label("Event Logs"), 0, row);
|
||||
GridPane.setMargin(getEventLogSpinner(), new Insets(2, 2, 2, 0));
|
||||
mEditorPane.add(getEventLogSpinner(), 1, row++);
|
||||
|
||||
mEditorPane.add(new Label("Recordings"), 0, row);
|
||||
GridPane.setMargin(getRecordingSpinner(), new Insets(2, 2, 2, 0));
|
||||
mEditorPane.add(getRecordingSpinner(), 1, row);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording directory maximum size threshold spinner
|
||||
* @return spinner
|
||||
*/
|
||||
private Spinner<Integer> getRecordingSpinner()
|
||||
{
|
||||
if(mRecordingSpinner == null)
|
||||
{
|
||||
mRecordingSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageRecordings(), 100);
|
||||
mRecordingSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||
.setDirectoryMaxUsageRecordings(newValue));
|
||||
}
|
||||
|
||||
return mRecordingSpinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event log directory maximum size threshold spinner
|
||||
* @return spinner
|
||||
*/
|
||||
private Spinner<Integer> getEventLogSpinner()
|
||||
{
|
||||
if(mEventLogSpinner == null)
|
||||
{
|
||||
mEventLogSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageEventLogs(), 100);
|
||||
mEventLogSpinner.setEditable(true);
|
||||
mEventLogSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||
.setDirectoryMaxUsageEventLogs(newValue));
|
||||
}
|
||||
|
||||
return mEventLogSpinner;
|
||||
}
|
||||
|
||||
private Label getApplicationRootLabel()
|
||||
{
|
||||
if(mApplicationRootLabel == null)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
462
src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
Normal file
462
src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
188
src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
Normal file
188
src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 -> {
|
||||
Tab tab = new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
||||
p25p1MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ public enum Form
|
||||
DTMF,
|
||||
ENCRYPTION_KEY,
|
||||
ESN,
|
||||
FULLY_QUALIFIED_IDENTIFIER,
|
||||
IPV4_ADDRESS,
|
||||
KNOX_TONE,
|
||||
LOCATION,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.identifier.alias;
|
||||
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Talker alias value provided by the network for the current talker (ie FROM).
|
||||
*/
|
||||
public class P25TalkerAliasIdentifier extends TalkerAliasIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param value of the talker alias
|
||||
*/
|
||||
public P25TalkerAliasIdentifier(String value)
|
||||
{
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return getValue() != null && !getValue().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
public static P25TalkerAliasIdentifier create(String value)
|
||||
{
|
||||
return new P25TalkerAliasIdentifier(value);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -284,11 +284,27 @@ public class DMRBurstFramer implements Listener<Dibit>
|
||||
}
|
||||
else
|
||||
{
|
||||
//When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
|
||||
//don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
|
||||
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
|
||||
//repeater or we have some form of mobile burst. If mobile, process to the correct timeslto, otherwise
|
||||
//dump the messages to timeslot 0 so that the burst doesn't corrupt the decoder states for either
|
||||
// timeslot, until we positively regain timeslot tracking
|
||||
if(burst1 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0);
|
||||
DMRSyncPattern sync1 = mSyncTrackerTimeslot1.getSyncPattern();
|
||||
|
||||
if(sync1.isMobileStationSyncPattern() || sync1.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 1);
|
||||
}
|
||||
else if(sync1.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
@@ -298,7 +314,21 @@ public class DMRBurstFramer implements Listener<Dibit>
|
||||
|
||||
if(burst2 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0);
|
||||
DMRSyncPattern sync2 = mSyncTrackerTimeslot2.getSyncPattern();
|
||||
|
||||
if(sync2.isMobileStationSyncPattern() || sync2.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 1);
|
||||
}
|
||||
else if(sync2.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -42,8 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -332,4 +335,38 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
{
|
||||
return DMRDecoder.this;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
File file = new File("/home/denny/Downloads/TIII,HYT414.585000,.wav");
|
||||
|
||||
try
|
||||
{
|
||||
ComplexWaveSource source = new ComplexWaveSource(file);
|
||||
source.open();
|
||||
System.out.println("Source Sample Rate: " + source.getSampleRate());
|
||||
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR());
|
||||
decoder.setMessageListener(message -> System.out.println("TS:" + message.getTimeslot() + " " + message));
|
||||
source.setListener(iNativeBuffer -> {
|
||||
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
ComplexSamples samples = it.next();
|
||||
decoder.receive(samples);
|
||||
}
|
||||
});
|
||||
decoder.setSampleRate(source.getSampleRate());
|
||||
decoder.start();
|
||||
source.start();
|
||||
|
||||
while(true)
|
||||
{
|
||||
source.next(65535);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
||||
@@ -424,18 +425,19 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
broadcast(packetEvent);
|
||||
|
||||
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
|
||||
}
|
||||
|
||||
if (geoPosition != null) {
|
||||
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(new IdentifierCollection(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,11 +446,18 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
*/
|
||||
private void processVoice(VoiceMessage message)
|
||||
{
|
||||
if(message.getSyncPattern().isDirectMode())
|
||||
if(message.getSyncPattern().isMobileSyncPattern())
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||
if(message.getSyncPattern().isDirectMode())
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "REPEATER", message.getTimestamp());
|
||||
}
|
||||
|
||||
//Use the timeslot as the talkgroup identifier since DCDM mode doesn't use talkgroups
|
||||
//Use the timeslot as the talkgroup identifier since DCDM & simple repeater modes don't use talkgroups
|
||||
getIdentifierCollection().update(DMRTalkgroup.create(getTimeslot()));
|
||||
}
|
||||
else
|
||||
@@ -700,7 +709,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case HYTERA_08_ANNOUNCEMENT:
|
||||
case HYTERA_68_ANNOUNCEMENT:
|
||||
case HYTERA_68_XPT_SITE_STATE:
|
||||
|
||||
break;
|
||||
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||
if(csbk instanceof HyteraTrafficChannelTalkerStatus status)
|
||||
{
|
||||
if(status.isChannelActive())
|
||||
{
|
||||
getIdentifierCollection().update(status.getIdentifiers());
|
||||
updateCurrentCall(DecodeEventType.CALL_GROUP, "HYTERA TIER 3 CALL", status.getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(status.getDestinationRadio());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
|
||||
if(csbk instanceof CapacityPlusNeighbors)
|
||||
{
|
||||
@@ -1209,19 +1233,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:
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
|
||||
@@ -71,6 +72,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
||||
private Listener<IMessage> mMessageListener;
|
||||
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
||||
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -93,6 +95,17 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Detect and correct messages employing an alternate CRC mask pattern.
|
||||
if(!message.isValid() && message instanceof CSBKMessage csbk)
|
||||
{
|
||||
mCrcMaskManager.check(csbk);
|
||||
}
|
||||
|
||||
if(message instanceof FullLCMessage flc)
|
||||
{
|
||||
if(flc.getTimeslot() == 1)
|
||||
@@ -153,10 +166,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
dispatch(message);
|
||||
|
||||
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
|
||||
if(message instanceof VoiceEMBMessage)
|
||||
if(message instanceof VoiceEMBMessage voice)
|
||||
{
|
||||
VoiceEMBMessage voice = (VoiceEMBMessage)message;
|
||||
|
||||
if(message.getTimeslot() == 1)
|
||||
{
|
||||
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
||||
|
||||
@@ -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
|
||||
@@ -91,9 +91,17 @@ public enum DMRSyncPattern
|
||||
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
||||
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
||||
|
||||
//Direct Mode Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
|
||||
DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
|
||||
//Direct Mode TS 1 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
|
||||
DIRECT_MODE_DATA_TIMESLOT_1);
|
||||
|
||||
//Direct Mode TS 2 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS2_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_2,
|
||||
DIRECT_MODE_DATA_TIMESLOT_2);
|
||||
|
||||
//Mobile Station Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> MOBILE_STATION_PATTERNS = EnumSet.of(MOBILE_STATION_DATA,
|
||||
MOBILE_STATION_VOICE, MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F);
|
||||
|
||||
/**
|
||||
* Pattern that represents the enum entry
|
||||
@@ -140,7 +148,43 @@ public enum DMRSyncPattern
|
||||
*/
|
||||
public boolean isDirectMode()
|
||||
{
|
||||
return DIRECT_MODE_PATTERNS.contains(this);
|
||||
return isDirectModeTS1() || isDirectModeTS2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 1
|
||||
* @return true if DMTS1
|
||||
*/
|
||||
public boolean isDirectModeTS1()
|
||||
{
|
||||
return DIRECT_MODE_TS1_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 2
|
||||
* @return true if DMTS2
|
||||
*/
|
||||
public boolean isDirectModeTS2()
|
||||
{
|
||||
return DIRECT_MODE_TS2_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile station sync pattern
|
||||
* @return true if a mobile station sync pattern.
|
||||
*/
|
||||
public boolean isMobileStationSyncPattern()
|
||||
{
|
||||
return MOBILE_STATION_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile sync pattern, either mobile station or direct mode.
|
||||
* @return true if a mobile sync pattern.
|
||||
*/
|
||||
public boolean isMobileSyncPattern()
|
||||
{
|
||||
return isMobileStationSyncPattern() || isDirectMode();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages alternate CRC masks that may be employed in a system.
|
||||
*/
|
||||
public class DmrCrcMaskManager
|
||||
{
|
||||
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
|
||||
private Map<Integer, ResidualCrcMaskTracker> mCsbkResidualTrackerMap = new TreeMap<>();
|
||||
private int mDominantMask = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DmrCrcMaskManager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy (ie clone) of all tracked values
|
||||
* @return list of tracker clones.
|
||||
*/
|
||||
public List<ResidualCrcMaskTracker> cloneTrackers()
|
||||
{
|
||||
List<ResidualCrcMaskTracker>trackers = new ArrayList<>();
|
||||
for(ResidualCrcMaskTracker tracker: mCsbkResidualTrackerMap.values())
|
||||
{
|
||||
trackers.add(tracker.clone());
|
||||
}
|
||||
|
||||
return trackers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the current tracked CRC values and counts.
|
||||
*/
|
||||
public void log()
|
||||
{
|
||||
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
|
||||
Collections.sort(trackers);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("DMR CRC Mask Manager - Current Tracked Values\n");
|
||||
for(ResidualCrcMaskTracker tracker: trackers)
|
||||
{
|
||||
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
|
||||
sb.append(" Count:").append(tracker.getCount());
|
||||
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
|
||||
}
|
||||
|
||||
LOGGER.info(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
|
||||
* if so, attempts to correct the message by using the alternate mask value once the alternate mask value is
|
||||
* detected to be used consistently enough.
|
||||
* @param csbk to re-check.
|
||||
*/
|
||||
public void check(CSBKMessage csbk)
|
||||
{
|
||||
if(!csbk.isValid())
|
||||
{
|
||||
int residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
|
||||
|
||||
if(mCsbkResidualTrackerMap.containsKey(residual))
|
||||
{
|
||||
mCsbkResidualTrackerMap.get(residual).increment();
|
||||
}
|
||||
else
|
||||
{
|
||||
mCsbkResidualTrackerMap.put(residual, new ResidualCrcMaskTracker(residual));
|
||||
}
|
||||
|
||||
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
|
||||
Collections.sort(trackers);
|
||||
|
||||
if(trackers.size() > 5)
|
||||
{
|
||||
mCsbkResidualTrackerMap.remove(trackers.get(0).getTrackedValue());
|
||||
}
|
||||
|
||||
if(trackers.size() > 0)
|
||||
{
|
||||
ResidualCrcMaskTracker dominant = trackers.get(trackers.size() - 1);
|
||||
|
||||
if(dominant.isValid())
|
||||
{
|
||||
mDominantMask = dominant.getTrackedValue();
|
||||
}
|
||||
}
|
||||
|
||||
if(mDominantMask != 0)
|
||||
{
|
||||
csbk.checkCRC(mDominantMask);
|
||||
//
|
||||
// if(csbk.isValid())
|
||||
// {
|
||||
// LOGGER.info("CSBK fixed using alternate mask: " + Integer.toHexString(mDominantMask).toUpperCase());
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Residual CRC mask value tracker. Tracks the number of observances of a residual CRC calculated value to detect
|
||||
* when an alternate CRC mask pattern is employed and automatically correct CRC values for messages.
|
||||
*/
|
||||
public class ResidualCrcMaskTracker implements Comparable<ResidualCrcMaskTracker>
|
||||
{
|
||||
private int mTrackedValue;
|
||||
private int mCount;
|
||||
private long mLastUpdated;
|
||||
|
||||
/**
|
||||
* Constructs an instance for the specified residual, sets the count to one and sets the last update to now.
|
||||
* @param residual value to track.
|
||||
*/
|
||||
public ResidualCrcMaskTracker(int residual)
|
||||
{
|
||||
mTrackedValue = residual;
|
||||
mCount = 1;
|
||||
mLastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep copy of this instance.
|
||||
* @return cloned instance.
|
||||
*/
|
||||
public ResidualCrcMaskTracker clone()
|
||||
{
|
||||
ResidualCrcMaskTracker clone = new ResidualCrcMaskTracker(getTrackedValue());
|
||||
clone.mCount = mCount;
|
||||
clone.mLastUpdated = mLastUpdated;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of number of times this residual has been observed.
|
||||
* @return count
|
||||
*/
|
||||
public int getCount()
|
||||
{
|
||||
return mCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the residual value has been observed at least 3x times indicating that it is possibly valid.
|
||||
* @return
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return mCount >= 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp when this value was last observed.
|
||||
*/
|
||||
public long getLastUpdated()
|
||||
{
|
||||
return mLastUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Residual tracked value.
|
||||
* @return residual
|
||||
*/
|
||||
public int getTrackedValue()
|
||||
{
|
||||
return mTrackedValue;
|
||||
}
|
||||
|
||||
public void increment()
|
||||
{
|
||||
mCount++;
|
||||
//Prevent rollover by keeping value at max integer value.
|
||||
if(mCount < 0)
|
||||
{
|
||||
mCount = Integer.MAX_VALUE;
|
||||
}
|
||||
mLastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom sort order by count and then by last updated timestamp.
|
||||
* @param o the object to be compared.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(ResidualCrcMaskTracker o)
|
||||
{
|
||||
int comparison = Integer.compare(getCount(), o.getCount());
|
||||
|
||||
if(comparison == 0)
|
||||
{
|
||||
comparison = Long.compare(getLastUpdated(), o.getLastUpdated());
|
||||
}
|
||||
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.identifier;
|
||||
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.location.LocationIdentifier;
|
||||
import io.github.dsheirer.identifier.location.Point;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* P25 location identifier for user radio reported GPS positions.
|
||||
*/
|
||||
public class P25Location extends LocationIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param value location
|
||||
* @param role to or from
|
||||
*/
|
||||
public P25Location(Point value, Role role)
|
||||
{
|
||||
super(value, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a new DMR location
|
||||
* @param latitude of the position
|
||||
* @param longitude of the position
|
||||
* @return constructed DMR location identifier
|
||||
*/
|
||||
public static P25Location createFrom(double latitude, double longitude)
|
||||
{
|
||||
Point point = new Point(latitude, longitude);
|
||||
return new P25Location(point, Role.FROM);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
|
||||
* *****************************************************************************
|
||||
* 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
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* 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.csbk;
|
||||
|
||||
@@ -72,6 +71,17 @@ public abstract class CSBKMessage extends DataMessage
|
||||
setValid(correctedBitCount < 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks CRC using an alternate mask value and sets the message valid flag according to the results.
|
||||
*/
|
||||
public void checkCRC(int mask)
|
||||
{
|
||||
int correctedBitCount = CRCDMR.correctCCITT80(getMessage(), 0, 80, mask);
|
||||
|
||||
//Set message valid flag according to the corrected bit count for the CRC protected message
|
||||
setValid(correctedBitCount < 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks crc for multi-block CSBK where the continuation block is an absolute channel parameters block
|
||||
*
|
||||
|
||||
@@ -29,8 +29,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAdjac
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko44;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko47;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraSmsAvailableNotification;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||
@@ -250,8 +250,8 @@ public class CSBKMessageFactory
|
||||
case HYTERA_08_CSBKO_44:
|
||||
csbk = new HyteraCsbko44(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case HYTERA_08_CSBKO_47:
|
||||
csbk = new HyteraCsbko47(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||
csbk = new HyteraTrafficChannelTalkerStatus(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case HYTERA_68_ACKNOWLEDGE:
|
||||
csbk = new Hytera68Acknowledge(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
|
||||
@@ -93,7 +93,7 @@ public enum Opcode
|
||||
//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"),
|
||||
HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
|
||||
|
||||
HYTERA_68_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA 68 XPT SITE STATE"),
|
||||
HYTERA_68_ALOHA(Vendor.HYTERA_68, 25, "HYTERA 68 ALOHA"),
|
||||
@@ -201,7 +201,7 @@ public enum Opcode
|
||||
* Hytera opcodes
|
||||
*/
|
||||
public static final EnumSet<Opcode> HYTERA = EnumSet.of(HYTERA_08_ACKNOWLEDGE, HYTERA_08_ANNOUNCEMENT,
|
||||
HYTERA_08_CSBKO_44, HYTERA_08_CSBKO_47, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
||||
HYTERA_08_CSBKO_44, HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
||||
HYTERA_68_ACKNOWLEDGE, HYTERA_68_ANNOUNCEMENT, HYTERA_68_XPT_PREAMBLE, HYTERA_68_CSBKO_62);
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,9 @@ import java.util.List;
|
||||
* Analysis: ... this was transmitted on the Tier III traffic channel during call termination. This CSBK was
|
||||
* interleaved with every other Terminator message, starting after the first 2x terminator messages. This continued
|
||||
* through the final teardown which was 3x Clear messages.
|
||||
*
|
||||
* On a second example, there were 8 distinct configurations of this message that were repeated in-order, multiple times
|
||||
* during the traffic channel shutdown ... a total of 51x of these messages were transmitted.
|
||||
*/
|
||||
public class HyteraCsbko44 extends CSBKMessage
|
||||
{
|
||||
|
||||
@@ -33,13 +33,14 @@ import java.util.List;
|
||||
/**
|
||||
* Hytera DMR Tier III - CSBKO 47 -
|
||||
*
|
||||
* Analysis: ... this was transmitted on the traffic channel while the mobile was at the end of a call, in the terminator
|
||||
* sequence. Since this comes from the SMS gateway 16776906, I wonder if this is a traffic channel message waiting notification?
|
||||
* Analysis: this message is transmitted three times at the beginning or end of a voice transmission and appears to
|
||||
* indicate that the traffic channel continues active and signals the start/end of an active speaker.
|
||||
*/
|
||||
public class HyteraCsbko47 extends CSBKMessage
|
||||
public class HyteraTrafficChannelTalkerStatus extends CSBKMessage
|
||||
{
|
||||
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static final int TALKER_INACTIVE = 30;
|
||||
private static final int[] DESTINATION_RADIO = 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};
|
||||
private static final int[] SOURCE_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
|
||||
@@ -59,7 +60,7 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
* @param timestamp
|
||||
* @param timeslot
|
||||
*/
|
||||
public HyteraCsbko47(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
public HyteraTrafficChannelTalkerStatus(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
@@ -79,17 +80,39 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
sb.append(" HYTERA CSBKO=47 ######### UNKNOWN");
|
||||
sb.append(" HYTERA TRAFFIC CHANNEL");
|
||||
if(isChannelInactive())
|
||||
{
|
||||
sb.append(" NOW AVAILABLE - PREVIOUS TALKER:").append(getDestinationRadio());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" IN USE - CURRENT TALKER:").append(getDestinationRadio());
|
||||
}
|
||||
sb.append(" FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" UNK1:").append(getUnknown1());
|
||||
sb.append(" UNK2:").append(getUnknown2());
|
||||
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the channel is inactive and available for use.
|
||||
*/
|
||||
public boolean isChannelInactive()
|
||||
{
|
||||
return getMessage().get(TALKER_INACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the channel is active and in-use by the radio identifiers
|
||||
*/
|
||||
public boolean isChannelActive()
|
||||
{
|
||||
return !isChannelInactive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Addressed (ie TO) radio identifier
|
||||
* @return radio identifier
|
||||
@@ -108,7 +131,6 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN_1, 2);
|
||||
}
|
||||
|
||||
public String getUnknown2()
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN_2, 2);
|
||||
@@ -118,7 +118,7 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
sb.append(" ENCRYPTED");
|
||||
}
|
||||
|
||||
sb.append(" CLEAR - RETURN TO ").append(getMoveToChannel());
|
||||
sb.append(" CLEAR - RETURN TO").append(getMoveToChannel());
|
||||
sb.append(" FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationId());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
|
||||
@@ -109,6 +109,15 @@ public class LCMessageFactory
|
||||
|
||||
LCOpcode opcode = FullLCMessage.getOpcode(message);
|
||||
|
||||
//Some Hytera Tier-3 systems use a zero-valued mask when the FLC is carried in the voice header or terminator
|
||||
//and in the same transmission it uses the standard mask when carried across the voice frames. It doesn't make
|
||||
//sense, but maybe it's a bug in their software implementation?
|
||||
if(!valid && opcode == LCOpcode.FULL_STANDARD_GROUP_VOICE_CHANNEL_USER)
|
||||
{
|
||||
//Retry the RS(12,9,4) with a mask value of zero
|
||||
valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, 0);
|
||||
}
|
||||
|
||||
FullLCMessage flc = null;
|
||||
|
||||
switch(opcode)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -33,35 +33,62 @@ public enum Vendor
|
||||
MOTOROLA_CONNECT_PLUS(6, "MOTOROLA CON+"),
|
||||
RADIO_DATA_GMBH(7, "RADIO DATA GMBH"),
|
||||
HYTERA_8(8, "HYTERA"),
|
||||
ASELSAN(9, "ASELSAN"),
|
||||
KIRISUN(10, "KIRISUN"),
|
||||
DMR_ASSOCIATION(11, "DMR ASSOCIATION"),
|
||||
SEPURA(12, "SEPURA"),
|
||||
ITALIA_RED_CROSS(13, "ITALIA RED CROSS"),
|
||||
MOTOROLA_CAPACITY_PLUS(16, "MOTOROLA CAP+"),
|
||||
EMC_SPA_19(19, "EMC SPA"),
|
||||
EMC_SPA_28(28, "EMC SPA"),
|
||||
RADIO_ACTIVITY_SRL_51(51, "RADIO ACTIVITY SRL"),
|
||||
RADIO_ACTIVITY_SRL_60(60, "RADIO ACTIVITY SRL"),
|
||||
MINISTERO_DELL_INTERNO(19, "ITALY MIN INTERIOR"),
|
||||
EMC_COMM_SRL_28(28, "EMC COMM SRL 28"),
|
||||
EMC_COMM_SRL_32(32, "EMC COMM SRL 32"),
|
||||
JVC_KENWOOD(51, "JVC-KENWOOD"),
|
||||
RADIO_ACTIVITY_SRL(60, "RADIO ACTIVITY SRL"),
|
||||
ZTE_TRUNKING(84, "ZTE TRUNKING"),
|
||||
TAIT(88, "TAIT"),
|
||||
HYTERA_68(104, "HYTERA"),
|
||||
VERTEX_STANDARD(119, "VERTEX STANDARD"),
|
||||
SIMOCO(120, "SIMOCO"),
|
||||
TEST(121, "TEST"),
|
||||
HYTERA_88(136, "HYTERA"),
|
||||
UNKNOWN(-1, "UNKNOWN");
|
||||
|
||||
private int mValue;
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param value decimal for the vendor
|
||||
* @param label for pretty print
|
||||
*/
|
||||
Vendor(int value, String label)
|
||||
{
|
||||
mValue = value;
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor ID value
|
||||
* @return value
|
||||
*/
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty print label
|
||||
* @return label
|
||||
*/
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default to use the label value instead.
|
||||
* @return string representation.
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
@@ -69,7 +96,7 @@ public enum Vendor
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of integer values to corresponding vendor entries
|
||||
* Quick lookup map of integer values to corresponding vendor entries
|
||||
*/
|
||||
private static final Map<Integer,Vendor> LOOKUP_MAP = new TreeMap<>();
|
||||
|
||||
|
||||
@@ -48,15 +48,13 @@ public class VoiceAMessage extends VoiceMessage
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(getSyncPattern().isDirectMode())
|
||||
if(!getSyncPattern().isMobileSyncPattern())
|
||||
{
|
||||
sb.append(getSyncPattern());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("CC:- ").append(getSyncPattern());
|
||||
sb.append("CC:- ");
|
||||
}
|
||||
|
||||
sb.append(getSyncPattern());
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public class VoiceEMBMessage extends VoiceMessage
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(getEMB().isValid())
|
||||
if(!getSyncPattern().isMobileSyncPattern() && getEMB().isValid())
|
||||
{
|
||||
sb.append("CC:").append(getEMB().getColorCode()).append(" ");
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
switch(getBandwidth())
|
||||
{
|
||||
case BW_7_5:
|
||||
return new ChannelSpecification(25000.0, 7500, 3500.0, 3750.0);
|
||||
case BW_12_5:
|
||||
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
|
||||
case BW_25_0:
|
||||
|
||||
@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdate;
|
||||
@@ -94,6 +95,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.isp.UMBTCTe
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.Opcode;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.HarrisGroupRegroupExplicitEncryptionCommand;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaDenyResponse;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrant;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrantUpdate;
|
||||
@@ -1221,6 +1223,21 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
mPatchGroupManager.removePatchGroups(tsbk.getIdentifiers());
|
||||
break;
|
||||
|
||||
//L3HARRIS PATCH GROUP OPCODES
|
||||
case HARRIS_OSP_GRG_EXENC_CMD:
|
||||
if(tsbk instanceof HarrisGroupRegroupExplicitEncryptionCommand regroup)
|
||||
{
|
||||
if(regroup.getRegroupOptions().isActivate())
|
||||
{
|
||||
mPatchGroupManager.addPatchGroup(regroup.getPatchGroup());
|
||||
}
|
||||
else
|
||||
{
|
||||
mPatchGroupManager.removePatchGroup(regroup.getPatchGroup());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
//STANDARD - INBOUND OPCODES
|
||||
case ISP_GROUP_VOICE_SERVICE_REQUEST:
|
||||
if(tsbk instanceof GroupVoiceServiceRequest)
|
||||
@@ -1885,6 +1902,22 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
case UNIT_TO_UNIT_ANSWER_REQUEST:
|
||||
processBroadcast(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Unit-to-Unit Answer Request");
|
||||
break;
|
||||
case MOTOROLA_UNIT_GPS:
|
||||
if(lcw instanceof LCMotorolaUnitGPS moto)
|
||||
{
|
||||
String gpsDetails = "LOCATION: " + moto.getLatitude() + " " + moto.getLongitude();
|
||||
|
||||
PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp)
|
||||
.location(new GeoPosition(moto.getLatitude(), moto.getLongitude()))
|
||||
.channel(getCurrentChannel())
|
||||
.details(gpsDetails)
|
||||
.end(timestamp)
|
||||
.protocol(Protocol.APCO25)
|
||||
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
|
||||
.build();
|
||||
broadcast(event);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// mLog.debug("Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() +
|
||||
// " OPCODE:" + lcw.getOpcodeNumber());
|
||||
|
||||
@@ -42,6 +42,7 @@ public class TerminatorMessageFilterSet extends FilterSet<IMessage>
|
||||
addFilter(new TerminatorMessageFilter("Command/Status", LinkControlOpcode.COMMAND_STATUS_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Network/Channel", LinkControlOpcode.NETWORK_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Voice", LinkControlOpcode.VOICE_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("L3Harris", LinkControlOpcode.L3HARRIS_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Motorola", LinkControlOpcode.MOTOROLA_OPCODES));
|
||||
|
||||
List<LinkControlOpcode> others = new ArrayList<>();
|
||||
|
||||
@@ -96,8 +96,14 @@ public enum LinkControlOpcode
|
||||
MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE("PATCH GROUP VOICE CHANNEL UPDATE", 1),
|
||||
MOTOROLA_PATCH_GROUP_ADD("PATCH GROUP ADD", 3),
|
||||
MOTOROLA_PATCH_GROUP_DELETE("PATCH GROUP DELETE", 4),
|
||||
MOTOROLA_UNIT_GPS("UNIT GPS", 6),
|
||||
MOTOROLA_TALK_COMPLETE("TALK_COMPLETE", 15),
|
||||
MOTOROLA_UNKNOWN("UNKNOWN", -1),
|
||||
MOTOROLA_UNKNOWN("MOTOROLA UNKNOWN", -1),
|
||||
|
||||
L3HARRIS_UNKNOWN_0A("UNKNOWN OPCODE 10", 10),
|
||||
L3HARRIS_UNKNOWN_2A("UNKNOWN OPCODE 42", 42),
|
||||
L3HARRIS_UNKNOWN_2B("UNKNOWN OPCODE 43", 43),
|
||||
L3HARRIS_UNKNOWN("L3HARRIS UNKNOWN", -1),
|
||||
|
||||
UNKNOWN("UNKNOWN", -1);
|
||||
|
||||
@@ -129,6 +135,11 @@ public enum LinkControlOpcode
|
||||
public static final EnumSet<LinkControlOpcode> MOTOROLA_OPCODES =
|
||||
EnumSet.range(MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER, MOTOROLA_UNKNOWN);
|
||||
|
||||
/**
|
||||
* L3Harris Opcodes
|
||||
*/
|
||||
public static final EnumSet<LinkControlOpcode> L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_UNKNOWN_0A,
|
||||
L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_UNKNOWN);
|
||||
|
||||
/**
|
||||
* Network/channel related opcodes
|
||||
@@ -190,6 +201,18 @@ public enum LinkControlOpcode
|
||||
return values()[value];
|
||||
}
|
||||
break;
|
||||
case HARRIS:
|
||||
switch(value)
|
||||
{
|
||||
case 10:
|
||||
return L3HARRIS_UNKNOWN_0A;
|
||||
case 42:
|
||||
return L3HARRIS_UNKNOWN_2A;
|
||||
case 43:
|
||||
return L3HARRIS_UNKNOWN_2B;
|
||||
default:
|
||||
return L3HARRIS_UNKNOWN;
|
||||
}
|
||||
case MOTOROLA:
|
||||
switch(value)
|
||||
{
|
||||
@@ -201,16 +224,18 @@ public enum LinkControlOpcode
|
||||
return MOTOROLA_PATCH_GROUP_ADD;
|
||||
case 4:
|
||||
return MOTOROLA_PATCH_GROUP_DELETE;
|
||||
case 6:
|
||||
return MOTOROLA_UNIT_GPS;
|
||||
case 15:
|
||||
return MOTOROLA_TALK_COMPLETE;
|
||||
default:
|
||||
return MOTOROLA_UNKNOWN;
|
||||
}
|
||||
default:
|
||||
if(0 <= value && value <= 63)
|
||||
{
|
||||
return values()[value];
|
||||
}
|
||||
}
|
||||
|
||||
if(0 <= value && value <= 63)
|
||||
{
|
||||
return values()[value];
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -15,17 +14,21 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.p25.phase1.message.lc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode10;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode42;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode43;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupAdd;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupDelete;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnknownOpcode;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcastExplicit;
|
||||
@@ -128,6 +131,15 @@ public class LinkControlWordFactory
|
||||
case UNIT_TO_UNIT_VOICE_CHANNEL_USER:
|
||||
return new LCUnitToUnitVoiceChannelUser(binaryMessage);
|
||||
|
||||
case L3HARRIS_UNKNOWN_0A:
|
||||
return new LCHarrisUnknownOpcode10(binaryMessage);
|
||||
case L3HARRIS_UNKNOWN_2A:
|
||||
return new LCHarrisUnknownOpcode42(binaryMessage);
|
||||
case L3HARRIS_UNKNOWN_2B:
|
||||
return new LCHarrisUnknownOpcode43(binaryMessage);
|
||||
case L3HARRIS_UNKNOWN:
|
||||
return new UnknownLinkControlWord(binaryMessage);
|
||||
|
||||
case MOTOROLA_PATCH_GROUP_ADD:
|
||||
return new LCMotorolaPatchGroupAdd(binaryMessage);
|
||||
case MOTOROLA_PATCH_GROUP_DELETE:
|
||||
@@ -138,6 +150,8 @@ public class LinkControlWordFactory
|
||||
return new LCMotorolaTalkComplete(binaryMessage);
|
||||
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE:
|
||||
return new LCMotorolaPatchGroupVoiceChannelUpdate(binaryMessage);
|
||||
case MOTOROLA_UNIT_GPS:
|
||||
return new LCMotorolaUnitGPS(binaryMessage);
|
||||
case MOTOROLA_UNKNOWN:
|
||||
return new LCMotorolaUnknownOpcode(binaryMessage);
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase1.message.lc.l3harris;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* L3Harris Unknown Opcode 10 (0x0A)
|
||||
* <p>
|
||||
* Observed in a traffic channel carried by a TDU during an SNDCP packet data session. The controller send an 'All
|
||||
* blocks received' PDU to the radio in the middle of a stream of these TDU/LC messages and the traffic channel ended
|
||||
* with Call Termination TDULC messages. So, this may be some form of current data session user information update.
|
||||
*/
|
||||
public class LCHarrisUnknownOpcode10 extends LinkControlWord
|
||||
{
|
||||
private static final int[] UNKNOWN_1 = {16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] TARGET_RADIO = {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 = {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 RadioIdentifier mSourceRadio;
|
||||
private RadioIdentifier mTargetRadio;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
/**
|
||||
* Constructs a Link Control Word from the binary message sequence.
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public LCHarrisUnknownOpcode10(BinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("**CRC-FAILED** ");
|
||||
}
|
||||
|
||||
if(isEncrypted())
|
||||
{
|
||||
sb.append(" ENCRYPTED");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("L3HARRIS UNKNOWN OPCODE 10 ");
|
||||
sb.append(" FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getTargetRadio());
|
||||
sb.append(" UNK:").append(getUnknown());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getUnknown()
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN_1, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Source radio identifier
|
||||
*/
|
||||
public RadioIdentifier getSourceRadio()
|
||||
{
|
||||
if(mSourceRadio == null)
|
||||
{
|
||||
mSourceRadio = APCO25RadioIdentifier.createFrom(getMessage().getInt(SOURCE_RADIO));
|
||||
}
|
||||
|
||||
return mSourceRadio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target radio identifier
|
||||
*/
|
||||
public RadioIdentifier getTargetRadio()
|
||||
{
|
||||
if(mTargetRadio == null)
|
||||
{
|
||||
mTargetRadio = APCO25RadioIdentifier.createTo(getMessage().getInt(TARGET_RADIO));
|
||||
}
|
||||
|
||||
return mTargetRadio;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of identifiers contained in this message
|
||||
*/
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifiers == null)
|
||||
{
|
||||
mIdentifiers = new ArrayList<>();
|
||||
mIdentifiers.add(getSourceRadio());
|
||||
}
|
||||
|
||||
return mIdentifiers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase1.message.lc.l3harris;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* L3Harris Unknown Opcode 42 (0x2A)
|
||||
* <p>
|
||||
* Observed in a voice traffic channel carried by an LDU1. This was transmitted once followed by an Opcode 43 (0x2B)
|
||||
* which is also unknown.
|
||||
*/
|
||||
public class LCHarrisUnknownOpcode42 extends LinkControlWord
|
||||
{
|
||||
/**
|
||||
* Constructs a Link Control Word from the binary message sequence.
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public LCHarrisUnknownOpcode42(BinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("**CRC-FAILED** ");
|
||||
}
|
||||
|
||||
if(isEncrypted())
|
||||
{
|
||||
sb.append(" ENCRYPTED");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("L3HARRIS UNKNOWN OPCODE 42 MSG:").append(getMessage().toHexString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of identifiers contained in this message
|
||||
*/
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.p25.phase1.message.lc.l3harris;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* L3Harris Unknown Opcode 43 (0x2B)
|
||||
* <p>
|
||||
* Observed in a voice traffic channel carried by an LDU1. This was transmitted once following an Opcode 42 (0x2A)
|
||||
* which is also unknown.
|
||||
*/
|
||||
public class LCHarrisUnknownOpcode43 extends LinkControlWord
|
||||
{
|
||||
/**
|
||||
* Constructs a Link Control Word from the binary message sequence.
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public LCHarrisUnknownOpcode43(BinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("**CRC-FAILED** ");
|
||||
}
|
||||
|
||||
if(isEncrypted())
|
||||
{
|
||||
sb.append(" ENCRYPTED");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("L3HARRIS UNKNOWN OPCODE 43 MSG:").append(getMessage().toHexString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of identifiers contained in this message
|
||||
*/
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user