mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
Compare commits
124 Commits
v0.6.0-alp
...
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 | ||
|
|
c96349fa1c | ||
|
|
f640576cc3 | ||
|
|
77d725a488 | ||
|
|
2c0305f928 | ||
|
|
21fd61d507 | ||
|
|
8fb56a5342 | ||
|
|
5d7b73f876 | ||
|
|
85b2a38058 | ||
|
|
c78517ee75 | ||
|
|
ae116d3dfe | ||
|
|
93252f9a40 | ||
|
|
3c03a4db2e | ||
|
|
d0fcd617aa | ||
|
|
6ca0decbd6 | ||
|
|
ddd9ca933e | ||
|
|
b2cc29569e | ||
|
|
411735772b | ||
|
|
2ffb2963af | ||
|
|
227c88c8a2 | ||
|
|
cbe9c79498 | ||
|
|
1b8c100b43 | ||
|
|
30c5fd64f2 | ||
|
|
3a6b486340 | ||
|
|
ded7335dbf | ||
|
|
005a4f8119 | ||
|
|
1ac49bfa7f |
2
.github/workflows/gradle.yml
vendored
2
.github/workflows/gradle.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 20
|
- name: Set up JDK 20
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
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">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" 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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||

|

|
||||||
|

|
||||||
|
|
||||||
# sdrtrunk
|
# sdrtrunk
|
||||||
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
|
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)
|
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
|
||||||
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
|
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
|
||||||
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
|
* [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.
|
* **(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.
|
* **(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
|
## Minimum System Requirements
|
||||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
||||||
* **CPU:** 4-core
|
* **CPU:** 4-core
|
||||||
|
|||||||
13
build.gradle
13
build.gradle
@@ -53,7 +53,8 @@ repositories {
|
|||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
|
|
||||||
version = '0.6.0-alpha7'
|
//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 20 is required for this version of the Project Panama preview/incubator feature
|
||||||
java {
|
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_suffix = "-full.tar.gz"
|
||||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
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_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_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||||
def jdk_download_osx_aarch64 = 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"
|
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.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||||
//jdk.accessibility is used with assistive technologies like screen readers
|
//jdk.accessibility is used with assistive technologies like screen readers
|
||||||
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.
|
//Use auto-detected modules and 'add' any specified modules.
|
||||||
rt.extension.additive.set(true)
|
rt.extension.additive.set(true)
|
||||||
@@ -339,7 +341,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Linux x86-64
|
//OSX x86-64
|
||||||
if(file(jdk_osx_x86_64).exists()) {
|
if(file(jdk_osx_x86_64).exists()) {
|
||||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||||
}
|
}
|
||||||
@@ -351,4 +353,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
|
|
||||||
configure(rt, jvmArgsLinux);
|
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
|
||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,37 +19,31 @@
|
|||||||
package io.github.dsheirer.alias;
|
package io.github.dsheirer.alias;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.action.AliasAction;
|
import io.github.dsheirer.alias.action.AliasAction;
|
||||||
import io.github.dsheirer.alias.action.AliasActionType;
|
|
||||||
import io.github.dsheirer.alias.action.beep.BeepAction;
|
import io.github.dsheirer.alias.action.beep.BeepAction;
|
||||||
import io.github.dsheirer.alias.action.clip.ClipAction;
|
import io.github.dsheirer.alias.action.clip.ClipAction;
|
||||||
import io.github.dsheirer.alias.action.script.ScriptAction;
|
import io.github.dsheirer.alias.action.script.ScriptAction;
|
||||||
import io.github.dsheirer.alias.id.AliasID;
|
import io.github.dsheirer.alias.id.AliasID;
|
||||||
import io.github.dsheirer.alias.id.AliasIDType;
|
|
||||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
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.esn.Esn;
|
||||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
|
||||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
|
||||||
import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||||
import io.github.dsheirer.alias.id.legacy.mpt1327.MPT1327ID;
|
|
||||||
import io.github.dsheirer.alias.id.legacy.nonrecordable.NonRecordable;
|
|
||||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||||
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.lojack.LoJackFunctionAndID;
|
||||||
import io.github.dsheirer.alias.id.priority.Priority;
|
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.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.record.Record;
|
import io.github.dsheirer.alias.id.record.Record;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
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.Talkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class AliasFactory
|
public class AliasFactory
|
||||||
{
|
{
|
||||||
@@ -64,6 +58,11 @@ public class AliasFactory
|
|||||||
BroadcastChannel copyBroadcast = new BroadcastChannel();
|
BroadcastChannel copyBroadcast = new BroadcastChannel();
|
||||||
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
|
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
|
||||||
return copyBroadcast;
|
return copyBroadcast;
|
||||||
|
case DCS:
|
||||||
|
Dcs originalDcs = (Dcs)id;
|
||||||
|
Dcs copyDcs = new Dcs();
|
||||||
|
copyDcs.setDCSCode(originalDcs.getDCSCode());
|
||||||
|
return copyDcs;
|
||||||
case ESN:
|
case ESN:
|
||||||
Esn originalESN = (Esn)id;
|
Esn originalESN = (Esn)id;
|
||||||
Esn copyESN = new Esn();
|
Esn copyESN = new Esn();
|
||||||
@@ -80,6 +79,18 @@ public class AliasFactory
|
|||||||
Min copyMin = new Min();
|
Min copyMin = new Min();
|
||||||
copyMin.setMin(originalMin.getMin());
|
copyMin.setMin(originalMin.getMin());
|
||||||
return copyMin;
|
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:
|
case PRIORITY:
|
||||||
Priority originalPriority = (Priority)id;
|
Priority originalPriority = (Priority)id;
|
||||||
Priority copyPriority = new Priority();
|
Priority copyPriority = new Priority();
|
||||||
@@ -187,6 +198,7 @@ public class AliasFactory
|
|||||||
copy.setGroup(original.getGroup());
|
copy.setGroup(original.getGroup());
|
||||||
copy.setColor(original.getColor());
|
copy.setColor(original.getColor());
|
||||||
copy.setIconName(original.getIconName());
|
copy.setIconName(original.getIconName());
|
||||||
|
copy.setRecordable(original.isRecordable());
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,65 +246,4 @@ public class AliasFactory
|
|||||||
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AliasID getAliasID(AliasIDType type)
|
|
||||||
{
|
|
||||||
switch(type)
|
|
||||||
{
|
|
||||||
case BROADCAST_CHANNEL:
|
|
||||||
return new BroadcastChannel();
|
|
||||||
case ESN:
|
|
||||||
return new Esn();
|
|
||||||
case FLEETSYNC:
|
|
||||||
return new FleetsyncID();
|
|
||||||
case LTR_NET_UID:
|
|
||||||
return new UniqueID();
|
|
||||||
case LOJACK:
|
|
||||||
return new LoJackFunctionAndID();
|
|
||||||
case MDC1200:
|
|
||||||
return new MDC1200ID();
|
|
||||||
case MIN:
|
|
||||||
return new Min();
|
|
||||||
case MPT1327:
|
|
||||||
return new MPT1327ID();
|
|
||||||
case NON_RECORDABLE:
|
|
||||||
return new NonRecordable();
|
|
||||||
case PRIORITY:
|
|
||||||
return new Priority();
|
|
||||||
case RADIO_ID:
|
|
||||||
return new Radio();
|
|
||||||
case RADIO_ID_RANGE:
|
|
||||||
return new RadioRange();
|
|
||||||
case RECORD:
|
|
||||||
return new Record();
|
|
||||||
case TALKGROUP:
|
|
||||||
return new Talkgroup();
|
|
||||||
case TALKGROUP_RANGE:
|
|
||||||
return new TalkgroupRange();
|
|
||||||
case SITE:
|
|
||||||
return new SiteID();
|
|
||||||
case STATUS:
|
|
||||||
return new UserStatusID();
|
|
||||||
case LEGACY_TALKGROUP:
|
|
||||||
return new LegacyTalkgroupID();
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unrecognized Alias ID type: " + type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AliasAction getAliasAction(AliasActionType type)
|
|
||||||
{
|
|
||||||
switch(type)
|
|
||||||
{
|
|
||||||
case BEEP:
|
|
||||||
return new BeepAction();
|
|
||||||
case CLIP:
|
|
||||||
return new ClipAction();
|
|
||||||
case SCRIPT:
|
|
||||||
return new ScriptAction();
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unrecognized Alias Action type: " + type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,32 +21,34 @@ package io.github.dsheirer.alias;
|
|||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
import io.github.dsheirer.alias.id.AliasID;
|
import io.github.dsheirer.alias.id.AliasID;
|
||||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
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.esn.Esn;
|
||||||
import io.github.dsheirer.alias.id.priority.Priority;
|
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.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
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.Talkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||||
|
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||||
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
||||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
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.radio.RadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
||||||
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
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.talkgroup.TalkgroupIdentifier;
|
||||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||||
|
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -57,6 +59,10 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of aliases that share the same alias list name and provides convenient methods for looking up alias
|
* List of aliases that share the same alias list name and provides convenient methods for looking up alias
|
||||||
@@ -67,6 +73,7 @@ public class AliasList
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
|
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
|
||||||
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
|
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
|
||||||
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
|
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
|
||||||
|
private Map<DCSCode,Alias> mDCSCodeAliasMap = new EnumMap<>(DCSCode.class);
|
||||||
private Map<String,Alias> mESNMap = new HashMap<>();
|
private Map<String,Alias> mESNMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
|
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
|
private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
|
||||||
@@ -152,6 +159,32 @@ public class AliasList
|
|||||||
|
|
||||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||||
break;
|
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:
|
case RADIO_ID:
|
||||||
Radio radio = (Radio)id;
|
Radio radio = (Radio)id;
|
||||||
|
|
||||||
@@ -178,6 +211,12 @@ public class AliasList
|
|||||||
|
|
||||||
radioRangeAliasList.add(radioRange, alias);
|
radioRangeAliasList.add(radioRange, alias);
|
||||||
break;
|
break;
|
||||||
|
case DCS:
|
||||||
|
if(id instanceof Dcs dcs)
|
||||||
|
{
|
||||||
|
mDCSCodeAliasMap.put(dcs.getDCSCode(), alias);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case ESN:
|
case ESN:
|
||||||
String esn = ((Esn)id).getEsn();
|
String esn = ((Esn)id).getEsn();
|
||||||
|
|
||||||
@@ -379,6 +418,8 @@ public class AliasList
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PATCH_GROUP:
|
case PATCH_GROUP:
|
||||||
|
List<Alias> aliases = new ArrayList<>();
|
||||||
|
|
||||||
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
||||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||||
|
|
||||||
@@ -386,8 +427,6 @@ public class AliasList
|
|||||||
|
|
||||||
if(patchGroupAliasList != null)
|
if(patchGroupAliasList != null)
|
||||||
{
|
{
|
||||||
List<Alias> aliases = new ArrayList<>();
|
|
||||||
|
|
||||||
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
||||||
|
|
||||||
if(alias != null)
|
if(alias != null)
|
||||||
@@ -395,19 +434,36 @@ public class AliasList
|
|||||||
aliases.add(alias);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return aliases;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case RADIO:
|
case RADIO:
|
||||||
RadioIdentifier radio = (RadioIdentifier)identifier;
|
RadioIdentifier radio = (RadioIdentifier)identifier;
|
||||||
|
|
||||||
@@ -439,9 +495,9 @@ public class AliasList
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TONE:
|
case TONE:
|
||||||
if(identifier instanceof ToneIdentifier)
|
if(identifier instanceof ToneIdentifier toneIdentifier)
|
||||||
{
|
{
|
||||||
ToneSequence toneSequence = ((ToneIdentifier)identifier).getValue();
|
ToneSequence toneSequence = toneIdentifier.getValue();
|
||||||
|
|
||||||
if(toneSequence != null && toneSequence.hasTones())
|
if(toneSequence != null && toneSequence.hasTones())
|
||||||
{
|
{
|
||||||
@@ -454,6 +510,15 @@ public class AliasList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(identifier instanceof DCSIdentifier dcsIdentifier)
|
||||||
|
{
|
||||||
|
DCSCode dcsCode = dcsIdentifier.getValue();
|
||||||
|
|
||||||
|
if(dcsCode != null)
|
||||||
|
{
|
||||||
|
return toList(mDCSCodeAliasMap.get(dcsCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -465,9 +530,7 @@ public class AliasList
|
|||||||
{
|
{
|
||||||
if(alias != null)
|
if(alias != null)
|
||||||
{
|
{
|
||||||
List<Alias> aliases = new ArrayList<>();
|
return Collections.singletonList(alias);
|
||||||
aliases.add(alias);
|
|
||||||
return aliases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@@ -590,6 +653,7 @@ public class AliasList
|
|||||||
*/
|
*/
|
||||||
public class TalkgroupAliasList
|
public class TalkgroupAliasList
|
||||||
{
|
{
|
||||||
|
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -599,6 +663,11 @@ public class AliasList
|
|||||||
|
|
||||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||||
|
{
|
||||||
|
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
|
||||||
|
}
|
||||||
|
|
||||||
int value = identifier.getValue();
|
int value = identifier.getValue();
|
||||||
|
|
||||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||||
@@ -619,6 +688,34 @@ public class AliasList
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void add(Talkgroup talkgroup, Alias alias)
|
public void add(Talkgroup talkgroup, Alias alias)
|
||||||
|
{
|
||||||
|
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||||
|
{
|
||||||
|
//Detect collisions
|
||||||
|
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||||
|
{
|
||||||
|
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
|
||||||
|
|
||||||
|
if(!existing.equals(alias))
|
||||||
|
{
|
||||||
|
fqt.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
|
{
|
||||||
|
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
|
//Detect talkgroup collisions and set overlap flag for both
|
||||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||||
@@ -641,6 +738,7 @@ public class AliasList
|
|||||||
|
|
||||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
||||||
{
|
{
|
||||||
@@ -672,6 +770,7 @@ public class AliasList
|
|||||||
*/
|
*/
|
||||||
public class RadioAliasList
|
public class RadioAliasList
|
||||||
{
|
{
|
||||||
|
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -681,6 +780,11 @@ public class AliasList
|
|||||||
|
|
||||||
public Alias getAlias(RadioIdentifier identifier)
|
public Alias getAlias(RadioIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||||
|
{
|
||||||
|
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
int value = identifier.getValue();
|
int value = identifier.getValue();
|
||||||
|
|
||||||
Alias mapValue = mRadioAliasMap.get(value);
|
Alias mapValue = mRadioAliasMap.get(value);
|
||||||
@@ -701,6 +805,34 @@ public class AliasList
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void add(Radio radio, Alias alias)
|
public void add(Radio radio, Alias alias)
|
||||||
|
{
|
||||||
|
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||||
|
{
|
||||||
|
//Detect collisions
|
||||||
|
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||||
|
{
|
||||||
|
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
|
||||||
|
|
||||||
|
if(!existing.equals(alias))
|
||||||
|
{
|
||||||
|
fqr.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
|
{
|
||||||
|
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
|
||||||
|
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
|
||||||
|
{
|
||||||
|
aliasID.setOverlap(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
//Detect collisions
|
//Detect collisions
|
||||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||||
@@ -713,7 +845,7 @@ public class AliasList
|
|||||||
|
|
||||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
{
|
{
|
||||||
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
|
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
|
||||||
{
|
{
|
||||||
aliasID.setOverlap(true);
|
aliasID.setOverlap(true);
|
||||||
}
|
}
|
||||||
@@ -723,6 +855,7 @@ public class AliasList
|
|||||||
|
|
||||||
mRadioAliasMap.put(radio.getValue(), alias);
|
mRadioAliasMap.put(radio.getValue(), alias);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void add(RadioRange radioRange, Alias alias)
|
public void add(RadioRange radioRange, Alias alias)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.alias.id;
|
||||||
|
|
||||||
@@ -26,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
|||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
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.esn.Esn;
|
||||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
||||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
||||||
@@ -37,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.legacy.uniqueID.UniqueID;
|
||||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||||
import io.github.dsheirer.alias.id.priority.Priority;
|
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.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.record.Record;
|
import io.github.dsheirer.alias.id.record.Record;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
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.Talkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||||
@@ -54,6 +54,7 @@ import javafx.util.Callback;
|
|||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
|
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
|
||||||
|
@JsonSubTypes.Type(value = Dcs.class, name = "dcs"),
|
||||||
@JsonSubTypes.Type(value = Esn.class, name = "esn"),
|
@JsonSubTypes.Type(value = Esn.class, name = "esn"),
|
||||||
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
|
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
|
||||||
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),
|
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),
|
||||||
@@ -62,6 +63,8 @@ import javafx.util.Callback;
|
|||||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
@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 = Priority.class, name = "priority"),
|
||||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.alias.id;
|
||||||
|
|
||||||
@@ -26,11 +23,14 @@ import java.util.EnumSet;
|
|||||||
public enum AliasIDType
|
public enum AliasIDType
|
||||||
{
|
{
|
||||||
BROADCAST_CHANNEL("Audio Broadcast Channel"),
|
BROADCAST_CHANNEL("Audio Broadcast Channel"),
|
||||||
|
DCS("Digital Coded Squelch (DCS)"),
|
||||||
ESN("ESN"),
|
ESN("ESN"),
|
||||||
INVERT("Audio Inversion"),
|
INVERT("Audio Inversion"),
|
||||||
LOJACK("LoJack"),
|
LOJACK("LoJack"),
|
||||||
LTR_NET_UID("LTR-Net UID"),
|
LTR_NET_UID("LTR-Net UID"),
|
||||||
MIN("Passport MIN"),
|
MIN("Passport MIN"),
|
||||||
|
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||||
|
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||||
PRIORITY("Audio Priority"),
|
PRIORITY("Audio Priority"),
|
||||||
RADIO_ID("Radio ID"),
|
RADIO_ID("Radio ID"),
|
||||||
RADIO_ID_RANGE("Radio ID Range"),
|
RADIO_ID_RANGE("Radio ID Range"),
|
||||||
@@ -58,7 +58,7 @@ public enum AliasIDType
|
|||||||
|
|
||||||
//Values used by the View-By alias editor
|
//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,
|
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()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
|||||||
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.dcs;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.alias.id.AliasID;
|
||||||
|
import io.github.dsheirer.alias.id.AliasIDType;
|
||||||
|
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digital Coded Squelch (DCS) tone identifier
|
||||||
|
*/
|
||||||
|
public class Dcs extends AliasID implements Comparable<Dcs>
|
||||||
|
{
|
||||||
|
private DCSCode mDCSCode;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AliasIDType getType()
|
||||||
|
{
|
||||||
|
return AliasIDType.DCS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(AliasID id)
|
||||||
|
{
|
||||||
|
if(isValid() && id instanceof Dcs other)
|
||||||
|
{
|
||||||
|
return other.isValid() && getDCSCode().equals(other.getDCSCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DCS code
|
||||||
|
* @return DCS code or null.
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "code")
|
||||||
|
public DCSCode getDCSCode()
|
||||||
|
{
|
||||||
|
return mDCSCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the DCS code value.
|
||||||
|
* @param dcsCode to set
|
||||||
|
*/
|
||||||
|
public void setDCSCode(DCSCode dcsCode)
|
||||||
|
{
|
||||||
|
mDCSCode = dcsCode;
|
||||||
|
updateValueProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return mDCSCode != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAudioIdentifier()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
if(isValid())
|
||||||
|
{
|
||||||
|
return getDCSCode().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "DCS-Invalid - No Tone Selected";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Dcs o)
|
||||||
|
{
|
||||||
|
if(isValid())
|
||||||
|
{
|
||||||
|
if(o.isValid())
|
||||||
|
{
|
||||||
|
return getDCSCode().compareTo(o.getDCSCode());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
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.icecast.IcecastConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||||
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
|
|||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||||
|
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
@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.RdioScannerBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
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.IcecastHTTPAudioBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||||
@@ -67,6 +70,9 @@ public class BroadcastFactory
|
|||||||
case RDIOSCANNER_CALL:
|
case RDIOSCANNER_CALL:
|
||||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||||
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
@@ -109,6 +115,8 @@ public class BroadcastFactory
|
|||||||
return new RdioScannerConfiguration(format);
|
return new RdioScannerConfiguration(format);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new BroadcastifyFeedConfiguration(format);
|
return new BroadcastifyFeedConfiguration(format);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzConfiguration(format);
|
||||||
case ICECAST_HTTP:
|
case ICECAST_HTTP:
|
||||||
return new IcecastHTTPConfiguration(format);
|
return new IcecastHTTPConfiguration(format);
|
||||||
case ICECAST_TCP:
|
case ICECAST_TCP:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public enum BroadcastServerType
|
|||||||
|
|
||||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||||
|
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||||
SHOUTCAST_V2("Shoutcast v2.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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -38,9 +38,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
|||||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
|
|||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.LinkedTransferQueue;
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
|
* 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();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
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());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
package io.github.dsheirer.audio.broadcast.rdioscanner;
|
package io.github.dsheirer.audio.broadcast.rdioscanner;
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
import io.github.dsheirer.alias.Alias;
|
import io.github.dsheirer.alias.Alias;
|
||||||
import io.github.dsheirer.alias.AliasList;
|
import io.github.dsheirer.alias.AliasList;
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
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.Form;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.IdentifierClass;
|
import io.github.dsheirer.identifier.IdentifierClass;
|
||||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
|
||||||
import io.github.dsheirer.identifier.Role;
|
import io.github.dsheirer.identifier.Role;
|
||||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
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.radio.RadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
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.IOException;
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
|
|||||||
import java.util.concurrent.LinkedTransferQueue;
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,6 +232,9 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
|||||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||||
String systemLabel = getSystemLabel(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
|
try
|
||||||
{
|
{
|
||||||
@@ -264,6 +255,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
|||||||
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
|
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
|
||||||
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
|
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
|
||||||
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
|
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
|
||||||
|
.addAudioName(audioName)
|
||||||
.addFile(audioBytes)
|
.addFile(audioBytes)
|
||||||
.addPart(FormField.DATE_TIME, timestampSeconds)
|
.addPart(FormField.DATE_TIME, timestampSeconds)
|
||||||
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
||||||
@@ -278,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
|||||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
|
||||||
.POST(bodyBuilder.build())
|
.POST(bodyBuilder.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -525,7 +516,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
|||||||
|
|
||||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
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());
|
sb.append(",").append(patched.getValue());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class RdioScannerBuilder
|
|||||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||||
private List<Part> mParts = new ArrayList<>();
|
private List<Part> mParts = new ArrayList<>();
|
||||||
private byte[] audioBytes = null;
|
private byte[] audioBytes = null;
|
||||||
|
private String audioName = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
@@ -51,6 +52,15 @@ public class RdioScannerBuilder
|
|||||||
return BOUNDARY;
|
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
|
* Adds a Audio bytes part to the call
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +120,8 @@ public class RdioScannerBuilder
|
|||||||
}
|
}
|
||||||
StringBuilder sb= new StringBuilder();
|
StringBuilder sb= new StringBuilder();
|
||||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
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();
|
return sb.toString();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
|
|||||||
|
|
||||||
import io.github.dsheirer.edac.CRC;
|
import io.github.dsheirer.edac.CRC;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.apache.commons.math3.util.FastMath;
|
import org.apache.commons.math3.util.FastMath;
|
||||||
@@ -844,13 +845,18 @@ public class BinaryMessage extends BitSet
|
|||||||
*/
|
*/
|
||||||
public int getTwosComplement(int start, int end)
|
public int getTwosComplement(int start, int end)
|
||||||
{
|
{
|
||||||
BinaryMessage sub = getSubMessage(start, end);
|
if(get(start))
|
||||||
boolean negative = sub.get(start);
|
{
|
||||||
sub.flip(0, sub.size());
|
//Negative value - flip and add one
|
||||||
|
BinaryMessage fragment = getSubMessage(start, end);
|
||||||
int value = sub.getInt(0, sub.size()) + 1;
|
fragment.flip(0, fragment.size());
|
||||||
value *= (negative ? -1 : 1);
|
return fragment.getInt(1, fragment.size()) + 1;
|
||||||
return value;
|
}
|
||||||
|
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);
|
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;
|
import javax.swing.JPanel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swing panel for Now Playing channels table and channel details tab set.
|
||||||
|
*/
|
||||||
public class NowPlayingPanel extends JPanel
|
public class NowPlayingPanel extends JPanel
|
||||||
{
|
{
|
||||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||||
@@ -40,45 +43,95 @@ public class NowPlayingPanel extends JPanel
|
|||||||
private final DecodeEventPanel mDecodeEventPanel;
|
private final DecodeEventPanel mDecodeEventPanel;
|
||||||
private final MessageActivityPanel mMessageActivityPanel;
|
private final MessageActivityPanel mMessageActivityPanel;
|
||||||
private final ChannelPowerPanel mChannelPowerPanel;
|
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,
|
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||||
* messages, events, and spectral view.
|
* messages, events, and spectral view.
|
||||||
*/
|
*/
|
||||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||||
SettingsManager settingsManager)
|
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||||
{
|
{
|
||||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||||
|
mDetailTabsVisible = detailTabsVisible;
|
||||||
|
|
||||||
init();
|
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()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||||
|
getSplitPane().add(mChannelMetadataPanel);
|
||||||
|
|
||||||
JideTabbedPane tabbedPane = new JideTabbedPane();
|
if(mDetailTabsVisible)
|
||||||
tabbedPane.addTab("Details", mChannelDetailPanel);
|
{
|
||||||
tabbedPane.addTab("Events", mDecodeEventPanel);
|
getSplitPane().add(getTabbedPane());
|
||||||
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);
|
|
||||||
|
|
||||||
|
add(getSplitPane());
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||||
|
|||||||
@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
|
|||||||
|
|
||||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||||
TunerManager tunerManager, UserPreferences userPreferences)
|
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||||
{
|
{
|
||||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||||
playlistManager.getAliasModel());
|
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);
|
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now playing panel.
|
||||||
|
*/
|
||||||
|
public NowPlayingPanel getNowPlayingPanel()
|
||||||
|
{
|
||||||
|
return mNowPlayingPanel;
|
||||||
|
}
|
||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
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("Map", mMapPanel);
|
||||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||||
|
|
||||||
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
|
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||||
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
|
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||||
|
|
||||||
//Set preferred size to influence the split between these panels
|
//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
|
* @param request containing channel and other details
|
||||||
* @throws ChannelException if a source is not available for the channel
|
* @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();
|
Channel channel = request.getChannel();
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
|||||||
*/
|
*/
|
||||||
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
|
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
|
//Sized to process 40 times per second
|
||||||
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
|
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
|
||||||
private FloatFFT_1D mFFT;
|
private FloatFFT_1D mFFT;
|
||||||
private float[] mInlineSamples;
|
private float[] mInlineSamples;
|
||||||
private float[] mInlineFilter;
|
private float[] mInlineFilter;
|
||||||
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
|||||||
*/
|
*/
|
||||||
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
|
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
|
//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
|
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.buffer.INativeBufferProvider;
|
|||||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
|
import io.github.dsheirer.log.LoggingSuppressor;
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||||
@@ -65,6 +66,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||||
{
|
{
|
||||||
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
||||||
|
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
|
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
|
||||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||||
@@ -108,7 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
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);
|
mBufferDispatcher.setListener(mNativeBufferReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +205,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
|
LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
|
||||||
|
channelSource = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
|||||||
mInputChannelCount = inputChannelCount;
|
mInputChannelCount = inputChannelCount;
|
||||||
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
|
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
|
||||||
mHeartbeatManager = heartbeatManager;
|
mHeartbeatManager = heartbeatManager;
|
||||||
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
|
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
|
||||||
50, mHeartbeatManager);
|
|
||||||
mChannelResultsDispatcher.setListener(floats -> {
|
mChannelResultsDispatcher.setListener(floats -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
125
src/main/java/io/github/dsheirer/edac/BPTC_16_2.java
Normal file
125
src/main/java/io/github/dsheirer/edac/BPTC_16_2.java
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.edac;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field.
|
||||||
|
* <p>
|
||||||
|
* See ETSI TS 102 361-1 B.2.2.1 and B.2.2.2
|
||||||
|
*/
|
||||||
|
public class BPTC_16_2
|
||||||
|
{
|
||||||
|
private static final int[] DEINTERLEAVE = new int[]{0, 24, 1, 25, 2, 26, 3, 27, 4, 28, 5, 29, 6, 30, 7, 31, 8, 16,
|
||||||
|
9, 17, 10, 18, 11, 19, 12, 20, 13, 21, 14, 22, 15, 23};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unscramble and perform FEC checks per paragraph B.2.2.1 for Non-Reverse Channel Single Burst
|
||||||
|
*
|
||||||
|
* @param message with 32 interleaved bits.
|
||||||
|
* @return descrambled and error checked message or null if the process fails or there are too many errors.
|
||||||
|
*/
|
||||||
|
public static CorrectedBinaryMessage decodeShortBurst(CorrectedBinaryMessage message)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||||
|
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
|
||||||
|
|
||||||
|
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check for even parity. Bits 0-15 should be the same as bits 16-31.
|
||||||
|
for(int x = 0; x < 16; x++)
|
||||||
|
{
|
||||||
|
if(deinterleaved.get(x) ^ deinterleaved.get(x + 16))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deinterleaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unscramble and perform FEC checks per paragraph B.2.2.2 for Reverse Channel Single Burst
|
||||||
|
*
|
||||||
|
* @param binaryMessage with 32 interleaved bits.
|
||||||
|
* @return descrambled and error checked message or null if the process fails or there are too many errors.
|
||||||
|
*/
|
||||||
|
public static CorrectedBinaryMessage decodeReverseChannel(CorrectedBinaryMessage message)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||||
|
System.out.println(" DEINTER: " + deinterleaved.toHexString());
|
||||||
|
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
|
||||||
|
System.out.println(" DECODED: " + deinterleaved.toHexString());
|
||||||
|
System.out.println("FEC:" + fec);
|
||||||
|
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check for odd parity. Bits 0-15 should be opposite of bits 16-31.
|
||||||
|
for(int x = 0; x < 16; x++)
|
||||||
|
{
|
||||||
|
if(deinterleaved.get(x) == deinterleaved.get(x + 16))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deinterleaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs deinterleave of the interleaved message.
|
||||||
|
*
|
||||||
|
* @param original to deinterleave
|
||||||
|
* @return deinterleaved message
|
||||||
|
*/
|
||||||
|
public static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage original)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage delinterleaved = new CorrectedBinaryMessage(32);
|
||||||
|
for(int x = 0; x < 32; x++)
|
||||||
|
{
|
||||||
|
if(original.get(x))
|
||||||
|
{
|
||||||
|
delinterleaved.set(DEINTERLEAVE[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return delinterleaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
String[] msgs = new String[]{"05030A03", "35003A00", "1C6D2C9E"};
|
||||||
|
|
||||||
|
for(String msg : msgs)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage original = new CorrectedBinaryMessage(BinaryMessage.loadHex(msg));
|
||||||
|
System.out.println("ORIGINAL: " + original.toHexString());
|
||||||
|
CorrectedBinaryMessage decoded = decodeReverseChannel(original);
|
||||||
|
System.out.println("-------------------------");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
* it under the terms of the GNU General Public License as published by
|
||||||
* later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
* 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.
|
* You should have received a copy of the GNU General Public License
|
||||||
* If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
*
|
* ****************************************************************************
|
||||||
******************************************************************************/
|
*/
|
||||||
package io.github.dsheirer.edac;
|
package io.github.dsheirer.edac;
|
||||||
|
|
||||||
import io.github.dsheirer.bits.BinaryMessage;
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
@@ -177,6 +180,28 @@ public class CRCDMR
|
|||||||
return 2;
|
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
|
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
|
||||||
* data blocks have a slightly complicated structure because the checksum
|
* data blocks have a slightly complicated structure because the checksum
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -226,17 +226,19 @@ public class CRCUtil
|
|||||||
{
|
{
|
||||||
mLog.debug("Starting");
|
mLog.debug("Starting");
|
||||||
|
|
||||||
//DMR message
|
long poly = 0x13l;
|
||||||
// String raw = "100110010001000000001001000000001111011011010000011001010000000000000000000000001100001000001011";
|
long[] checksums = generate(32, 4, poly, 0, true);
|
||||||
String raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101001001111010001110";
|
|
||||||
mLog.debug(raw);
|
|
||||||
raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101000000000000000000";
|
|
||||||
BinaryMessage message = BinaryMessage.load(raw);
|
|
||||||
mLog.debug(message.toString());
|
|
||||||
|
|
||||||
long polynomial = 0x11021l;
|
StringBuilder sb = new StringBuilder();
|
||||||
decode(message, 0, 80, polynomial, 16);
|
sb.append("private static int[] CHECKSUMS = new int[]{");
|
||||||
mLog.debug(message.toString());
|
for(long checksum: checksums)
|
||||||
mLog.debug("Finished");
|
{
|
||||||
|
sb.append("0x").append(Long.toHexString(checksum).toUpperCase());
|
||||||
|
sb.append(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("};");
|
||||||
|
|
||||||
|
System.out.println("Checksums:\n" + sb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
package io.github.dsheirer.edac;
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
import io.github.dsheirer.bits.BinaryMessage;
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
* SDR Trunk
|
|
||||||
* Copyright (C) 2014 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,10 +14,15 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* -----------------------------------------------------------------------
|
* ****************************************************************************
|
||||||
* Galois24 decoder based on Hank Wallace's tutorial/algorithm located at:
|
*/
|
||||||
* http://www.aqdi.com/golay.htm
|
|
||||||
******************************************************************************/
|
package io.github.dsheirer.edac;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.BinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Galois 24/12/7 decoder
|
* Galois 24/12/7 decoder
|
||||||
@@ -160,4 +158,17 @@ public class Golay24
|
|||||||
|
|
||||||
return (checksum ^ calculated);
|
return (checksum ^ calculated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F3BB20"));
|
||||||
|
// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F0C5C0"));
|
||||||
|
CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("AFAC00"));
|
||||||
|
|
||||||
|
System.out.println("M:" + bm.toHexString());
|
||||||
|
int a = Golay24.checkAndCorrect(bm, 0);
|
||||||
|
System.out.println("M:" + bm.toHexString());
|
||||||
|
|
||||||
|
System.out.println("A:" + a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ChannelMap;
|
||||||
import io.github.dsheirer.controller.channel.map.ChannelRange;
|
import io.github.dsheirer.controller.channel.map.ChannelRange;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
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.IconManager;
|
||||||
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
|
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
|
||||||
import io.github.dsheirer.gui.playlist.PlaylistEditor;
|
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.UserPreferencesEditor;
|
||||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
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.icon.IconModel;
|
||||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||||
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
||||||
import io.github.dsheirer.module.log.EventLogManager;
|
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.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
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 USER_PREFERENCES_EDITOR = "preferences";
|
||||||
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
|
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_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_ICON_MANAGER_EDITOR = "icon.manager";
|
||||||
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
|
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
|
||||||
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
|
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
|
||||||
@@ -82,14 +84,15 @@ public class JavaFxWindowManager extends Application
|
|||||||
private TunerManager mTunerManager;
|
private TunerManager mTunerManager;
|
||||||
private UserPreferences mUserPreferences;
|
private UserPreferences mUserPreferences;
|
||||||
private UserPreferencesEditor mUserPreferencesEditor;
|
private UserPreferencesEditor mUserPreferencesEditor;
|
||||||
private DMRRecordingViewer mDmrRecordingViewer;
|
private RecordingViewer mRecordingViewer;
|
||||||
|
|
||||||
private Stage mChannelMapStage;
|
private Stage mChannelMapStage;
|
||||||
private Stage mIconManagerStage;
|
private Stage mIconManagerStage;
|
||||||
private Stage mJmbeEditorStage;
|
private Stage mJmbeEditorStage;
|
||||||
private Stage mPlaylistStage;
|
private Stage mPlaylistStage;
|
||||||
private Stage mUserPreferencesStage;
|
private Stage mUserPreferencesStage;
|
||||||
private Stage mDmrRecordingViewerStage;
|
private Stage mRecordingViewerStage;
|
||||||
|
private JFXPanel mStatusPanel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance. Note: this constructor is used for Swing applications.
|
* Constructs an instance. Note: this constructor is used for Swing applications.
|
||||||
@@ -118,6 +121,27 @@ public class JavaFxWindowManager extends Application
|
|||||||
setup();
|
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()
|
private void setup()
|
||||||
{
|
{
|
||||||
//Register this class to receive events via each method annotated with @Subscribe
|
//Register this class to receive events via each method annotated with @Subscribe
|
||||||
@@ -164,27 +188,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
public void shutdown()
|
public void shutdown()
|
||||||
{
|
{
|
||||||
MyEventBus.getGlobalEventBus().unregister(this);
|
MyEventBus.getGlobalEventBus().unregister(this);
|
||||||
|
mUserPreferences.getJavaFxPreferences().clearStageMonitors();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.exit();
|
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();
|
createJFXPanel();
|
||||||
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800);
|
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
|
||||||
mDmrRecordingViewerStage = new Stage();
|
mRecordingViewerStage = new Stage();
|
||||||
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer");
|
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
|
||||||
mDmrRecordingViewerStage.setScene(scene);
|
mRecordingViewerStage.setScene(scene);
|
||||||
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER);
|
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()
|
public Stage getIconManagerStage()
|
||||||
@@ -264,9 +268,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getJmbeEditorStage().show();
|
restoreStage(getJmbeEditorStage());
|
||||||
getJmbeEditorStage().requestFocus();
|
|
||||||
getJmbeEditorStage().toFront();
|
|
||||||
getJmbeEditor().process(request);
|
getJmbeEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -337,9 +339,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
execute(() -> {
|
execute(() -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
getPlaylistStage().show();
|
restoreStage(getPlaylistStage());
|
||||||
getPlaylistStage().requestFocus();
|
|
||||||
getPlaylistStage().toFront();
|
|
||||||
getPlaylistEditor().process(request);
|
getPlaylistEditor().process(request);
|
||||||
}
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
@@ -387,9 +387,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
public void process(final ViewUserPreferenceEditorRequest request)
|
public void process(final ViewUserPreferenceEditorRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getUserPreferencesStage().show();
|
restoreStage(getUserPreferencesStage());
|
||||||
getUserPreferencesStage().requestFocus();
|
|
||||||
getUserPreferencesStage().toFront();
|
|
||||||
getUserPreferencesEditor().process(request);
|
getUserPreferencesEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -428,11 +426,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void process(final ViewIconManagerRequest request)
|
public void process(final ViewIconManagerRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> restoreStage(getIconManagerStage()));
|
||||||
getIconManagerStage().show();
|
|
||||||
getIconManagerStage().requestFocus();
|
|
||||||
getIconManagerStage().toFront();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -442,9 +436,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
public void process(final ViewChannelMapEditorRequest request)
|
public void process(final ViewChannelMapEditorRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getChannelMapStage().show();
|
restoreStage(getChannelMapStage());
|
||||||
getChannelMapStage().requestFocus();
|
|
||||||
getChannelMapStage().toFront();
|
|
||||||
getChannelMapEditor().process(request);
|
getChannelMapEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -453,13 +445,24 @@ public class JavaFxWindowManager extends Application
|
|||||||
* Process a channel map editor request
|
* Process a channel map editor request
|
||||||
*/
|
*/
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void process(final ViewDmrRecordingViewerRequest request)
|
public void process(final ViewRecordingViewerRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> restoreStage(getRecordingViewerStage()));
|
||||||
getDmrRecordingViewerStage().show();
|
}
|
||||||
getDmrRecordingViewerStage().requestFocus();
|
|
||||||
getDmrRecordingViewerStage().toFront();
|
/**
|
||||||
});
|
* 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
|
@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.ChannelException;
|
||||||
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
|
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
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.icon.ViewIconManagerRequest;
|
||||||
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
|
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
|
||||||
import io.github.dsheirer.gui.preference.CalibrateRequest;
|
import io.github.dsheirer.gui.preference.CalibrateRequest;
|
||||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
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.icon.IconModel;
|
||||||
import io.github.dsheirer.log.ApplicationLog;
|
import io.github.dsheirer.log.ApplicationLog;
|
||||||
import io.github.dsheirer.map.MapService;
|
import io.github.dsheirer.map.MapService;
|
||||||
import io.github.dsheirer.module.log.EventLogManager;
|
import io.github.dsheirer.module.log.EventLogManager;
|
||||||
|
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
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.DiscoveredTuner;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
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.ShowTunerMenuItem;
|
||||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
@@ -68,11 +69,11 @@ import java.awt.GraphicsEnvironment;
|
|||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
import java.awt.Robot;
|
import java.awt.Robot;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.WindowAdapter;
|
import java.awt.event.WindowAdapter;
|
||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -80,7 +81,9 @@ import java.nio.file.Path;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.prefs.Preferences;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.embed.swing.JFXPanel;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import jiconfont.icons.font_awesome.FontAwesome;
|
import jiconfont.icons.font_awesome.FontAwesome;
|
||||||
import jiconfont.swing.IconFontSwing;
|
import jiconfont.swing.IconFontSwing;
|
||||||
@@ -105,14 +108,19 @@ import javax.swing.plaf.metal.MetalLookAndFeel;
|
|||||||
public class SDRTrunk implements Listener<TunerEvent>
|
public class SDRTrunk implements Listener<TunerEvent>
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
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 BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
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 SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||||
|
|
||||||
private boolean mBroadcastStatusVisible;
|
private boolean mBroadcastStatusVisible;
|
||||||
|
private boolean mResourceStatusVisible;
|
||||||
|
private boolean mNowPlayingDetailsVisible;
|
||||||
private AudioRecordingManager mAudioRecordingManager;
|
private AudioRecordingManager mAudioRecordingManager;
|
||||||
private AudioStreamingManager mAudioStreamingManager;
|
private AudioStreamingManager mAudioStreamingManager;
|
||||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||||
@@ -127,6 +135,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
private UserPreferences mUserPreferences = new UserPreferences();
|
private UserPreferences mUserPreferences = new UserPreferences();
|
||||||
private TunerManager mTunerManager;
|
private TunerManager mTunerManager;
|
||||||
private ApplicationLog mApplicationLog;
|
private ApplicationLog mApplicationLog;
|
||||||
|
private ResourceMonitor mResourceMonitor;
|
||||||
|
private JFXPanel mResourceStatusPanel;
|
||||||
|
|
||||||
private String mTitle;
|
private String mTitle;
|
||||||
|
|
||||||
@@ -140,6 +150,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||||
mApplicationLog.start();
|
mApplicationLog.start();
|
||||||
|
|
||||||
|
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||||
|
|
||||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||||
|
|
||||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
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();
|
ThreadPool.logSettings();
|
||||||
|
|
||||||
mLog.info("Home path: " + home.toString());
|
|
||||||
|
|
||||||
//Load properties file
|
//Load properties file
|
||||||
if(home != null)
|
loadProperties();
|
||||||
{
|
|
||||||
loadProperties(home);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Log current properties setting
|
//Log current properties setting
|
||||||
SystemProperties.getInstance().logCurrentSettings();
|
SystemProperties.getInstance().logCurrentSettings();
|
||||||
@@ -213,10 +217,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
MapService mapService = new MapService(mIconModel);
|
MapService mapService = new MapService(mIconModel);
|
||||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||||
|
|
||||||
|
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||||
|
|
||||||
if(!GraphicsEnvironment.isHeadless())
|
if(!GraphicsEnvironment.isHeadless())
|
||||||
{
|
{
|
||||||
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
||||||
mSettingsManager, mTunerManager, mUserPreferences);
|
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||||
@@ -323,7 +329,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
*/
|
*/
|
||||||
private void initGUI()
|
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
|
* Setup main JFrame window
|
||||||
@@ -379,7 +385,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mSplitPane.add(mSpectralPanel);
|
mSplitPane.add(mSpectralPanel);
|
||||||
mSplitPane.add(mControllerPanel);
|
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
|
//Show broadcast status panel when user requests - disabled by default
|
||||||
if(mBroadcastStatusVisible)
|
if(mBroadcastStatusVisible)
|
||||||
@@ -389,6 +395,13 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
|
|
||||||
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
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
|
* Menu items
|
||||||
*/
|
*/
|
||||||
@@ -399,15 +412,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
menuBar.add(fileMenu);
|
menuBar.add(fileMenu);
|
||||||
|
|
||||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||||
exitMenu.addActionListener(
|
exitMenu.addActionListener(event -> {
|
||||||
new ActionListener()
|
|
||||||
{
|
|
||||||
public void actionPerformed(ActionEvent event)
|
|
||||||
{
|
|
||||||
processShutdown();
|
processShutdown();
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fileMenu.add(exitMenu);
|
fileMenu.add(exitMenu);
|
||||||
@@ -415,28 +423,40 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
JMenu viewMenu = new JMenu("View");
|
JMenu viewMenu = new JMenu("View");
|
||||||
|
|
||||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||||
|
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||||
viewMenu.add(viewPlaylistItem);
|
viewMenu.add(viewPlaylistItem);
|
||||||
|
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
|
|
||||||
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
|
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||||
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
|
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||||
viewMenu.add(dmrMessageViewerMenu);
|
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||||
|
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||||
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)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Desktop.getDesktop().open(getHomePath().toFile());
|
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: " + logsDirectory,
|
||||||
|
"Can't launch file explorer",
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@@ -444,27 +464,80 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
|
|
||||||
JOptionPane.showMessageDialog(mMainGui,
|
JOptionPane.showMessageDialog(mMainGui,
|
||||||
"Can't launch file explorer - files are located at: " +
|
"Can't launch file explorer - files are located at: " +
|
||||||
getHomePath().toString(),
|
recordingsDirectory,
|
||||||
"Can't launch file explorer",
|
"Can't launch file explorer",
|
||||||
JOptionPane.ERROR_MESSAGE);
|
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(logFilesMenu);
|
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");
|
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||||
|
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||||
viewMenu.add(preferencesItem);
|
viewMenu.add(preferencesItem);
|
||||||
|
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
viewMenu.add(new TunersMenu());
|
viewMenu.add(new TunersMenu());
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
|
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
|
||||||
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
|
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||||
|
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||||
|
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||||
|
|
||||||
menuBar.add(viewMenu);
|
menuBar.add(viewMenu);
|
||||||
|
|
||||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||||
|
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||||
@@ -515,6 +588,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mLog.info("Stopping channels ...");
|
mLog.info("Stopping channels ...");
|
||||||
mPlaylistManager.getChannelProcessingManager().shutdown();
|
mPlaylistManager.getChannelProcessingManager().shutdown();
|
||||||
mAudioRecordingManager.stop();
|
mAudioRecordingManager.stop();
|
||||||
|
mResourceMonitor.stop();
|
||||||
|
|
||||||
mLog.info("Stopping spectral display ...");
|
mLog.info("Stopping spectral display ...");
|
||||||
mSpectralPanel.clearTuner();
|
mSpectralPanel.clearTuner();
|
||||||
@@ -560,7 +634,54 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mMainGui.revalidate();
|
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,
|
* Loads the application properties file from the user's home directory,
|
||||||
* creating the properties file for the first-time, if necessary
|
* 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
|
try
|
||||||
{
|
{
|
||||||
mLog.info("SDRTrunk - creating application properties file [" +
|
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
|
||||||
propsPath.toAbsolutePath() + "]");
|
Files.createFile(propertiesPath);
|
||||||
|
|
||||||
Files.createFile(propsPath);
|
|
||||||
}
|
}
|
||||||
catch(IOException e)
|
catch(IOException e)
|
||||||
{
|
{
|
||||||
mLog.error("SDRTrunk - couldn't create application properties "
|
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
|
||||||
+ "file [" + propsPath.toAbsolutePath(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Files.exists(propsPath))
|
if(Files.exists(propertiesPath))
|
||||||
{
|
{
|
||||||
SystemProperties.getInstance().load(propsPath);
|
SystemProperties.getInstance().load(propertiesPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
|
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
|
||||||
"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
|
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||||
{
|
{
|
||||||
private ControllerPanel mControllerPanel;
|
public BroadcastStatusVisibleMenuItem()
|
||||||
|
|
||||||
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
|
|
||||||
{
|
{
|
||||||
super("Show Streaming Status");
|
super("Show Streaming Status");
|
||||||
|
setSelected(mBroadcastStatusVisible);
|
||||||
mControllerPanel = controllerPanel;
|
addActionListener(e -> {
|
||||||
|
|
||||||
setSelected(mBroadcastStatusPanel != null);
|
|
||||||
|
|
||||||
addActionListener(new ActionListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e)
|
|
||||||
{
|
|
||||||
toggleBroadcastStatusPanelVisibility();
|
toggleBroadcastStatusPanelVisibility();
|
||||||
setSelected(mBroadcastStatusVisible);
|
setSelected(mBroadcastStatusVisible);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.gui.control;
|
||||||
|
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.control.TextFormatter;
|
import javafx.scene.control.TextFormatter;
|
||||||
import javafx.util.converter.IntegerStringConverter;
|
import javafx.util.converter.IntegerStringConverter;
|
||||||
|
|
||||||
import java.util.function.UnaryOperator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text field control for integer values.
|
* Text field control for integer values.
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +48,7 @@ public class IntegerTextField extends TextField
|
|||||||
return null;
|
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
|
//Do nothing ... we couldn't parse the value
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -22,22 +22,9 @@ import io.github.dsheirer.source.ISourceEventProcessor;
|
|||||||
import io.github.dsheirer.source.InvalidFrequencyException;
|
import io.github.dsheirer.source.InvalidFrequencyException;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import net.miginfocom.swing.MigLayout;
|
|
||||||
import org.apache.commons.math3.util.FastMath;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.JTextField;
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Cursor;
|
|
||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.awt.Image;
|
|
||||||
import java.awt.Point;
|
|
||||||
import java.awt.Toolkit;
|
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.KeyListener;
|
import java.awt.event.KeyListener;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
@@ -49,27 +36,39 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
import org.apache.commons.math3.util.FastMath;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom frequency controller for displaying or editing frequency values in MHz.
|
||||||
|
*/
|
||||||
public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class);
|
private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class);
|
||||||
|
|
||||||
private List<ISourceEventProcessor> mProcessors = new ArrayList<>();
|
private List<ISourceEventProcessor> mProcessors = new ArrayList<>();
|
||||||
|
|
||||||
private Color mHighlightColor = Color.YELLOW;
|
private Color mHighlightColor = Color.YELLOW;
|
||||||
|
|
||||||
private long mFrequency;
|
private long mFrequency;
|
||||||
|
|
||||||
private Cursor mBlankCursor;
|
|
||||||
|
|
||||||
private HashMap<Integer,Digit> mDigits = new HashMap<Integer,Digit>();
|
private HashMap<Integer,Digit> mDigits = new HashMap<Integer,Digit>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
public JFrequencyControl()
|
public JFrequencyControl()
|
||||||
{
|
{
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the control/layout.
|
||||||
|
*/
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", ""));
|
setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", ""));
|
||||||
@@ -104,19 +103,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a blank cursor to use when the mouse is over the digits
|
|
||||||
*/
|
|
||||||
//Create an empty byte array
|
|
||||||
byte[] imageByte = new byte[0];
|
|
||||||
|
|
||||||
//Create image for cursor using empty array
|
|
||||||
Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte);
|
|
||||||
|
|
||||||
mBlankCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage, new Point(0, 0), "cursor");
|
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the control.
|
||||||
|
* @param enabled true if this component should be enabled, false otherwise
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setEnabled(boolean enabled)
|
public void setEnabled(boolean enabled)
|
||||||
{
|
{
|
||||||
@@ -157,11 +150,18 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current frequency value for the control.
|
||||||
|
* @return frequency
|
||||||
|
*/
|
||||||
public long getFrequency()
|
public long getFrequency()
|
||||||
{
|
{
|
||||||
return mFrequency;
|
return mFrequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the frequency value according to the manually changed digit value.
|
||||||
|
*/
|
||||||
private void updateFrequency()
|
private void updateFrequency()
|
||||||
{
|
{
|
||||||
long frequency = 0;
|
long frequency = 0;
|
||||||
@@ -174,14 +174,15 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
mFrequency = frequency;
|
mFrequency = frequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires a source changed event
|
||||||
|
* @throws SourceException of there's an error
|
||||||
|
*/
|
||||||
private void fireSourceChanged() throws SourceException
|
private void fireSourceChanged() throws SourceException
|
||||||
{
|
{
|
||||||
updateFrequency();
|
updateFrequency();
|
||||||
|
|
||||||
Iterator<ISourceEventProcessor> it = mProcessors.iterator();
|
Iterator<ISourceEventProcessor> it = mProcessors.iterator();
|
||||||
|
|
||||||
SourceEvent event = SourceEvent.frequencyRequest(mFrequency);
|
SourceEvent event = SourceEvent.frequencyRequest(mFrequency);
|
||||||
|
|
||||||
while(it.hasNext())
|
while(it.hasNext())
|
||||||
{
|
{
|
||||||
it.next().process(event);
|
it.next().process(event);
|
||||||
@@ -217,12 +218,21 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
mProcessors.remove(processor);
|
mProcessors.remove(processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom single digit value editor.
|
||||||
|
*/
|
||||||
public class Digit extends JTextField
|
public class Digit extends JTextField
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
private int mPower = 0;
|
private int mPower = 0;
|
||||||
private long mValue = 0;
|
private long mValue = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param position of the digit where a position of 1 is the ones unit of MHz and a position of -1 is the tenths
|
||||||
|
* unit of MHz.
|
||||||
|
* @throws ParseException if there is an error
|
||||||
|
*/
|
||||||
private Digit(int position) throws ParseException
|
private Digit(int position) throws ParseException
|
||||||
{
|
{
|
||||||
super("0");
|
super("0");
|
||||||
@@ -236,6 +246,11 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
this.addMouseWheelListener(listener);
|
this.addMouseWheelListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a custom tooltip for the current hover-over digit.
|
||||||
|
* @param power of the digit in units of 10
|
||||||
|
* @return custom tooptip.
|
||||||
|
*/
|
||||||
private static String getTooltip(int power)
|
private static String getTooltip(int power)
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@@ -288,23 +303,32 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
{
|
{
|
||||||
//Strip the digits higher than this one
|
//Strip the digits higher than this one
|
||||||
long lower = frequency % (long)(FastMath.pow(10, mPower + 1));
|
long lower = frequency % (long)(FastMath.pow(10, mPower + 1));
|
||||||
|
|
||||||
//Set the value to int value of dividing by 10 to this power
|
//Set the value to int value of dividing by 10 to this power
|
||||||
long value = (long)(lower / (long)(FastMath.pow(10, mPower)));
|
long value = (long)(lower / (long)(FastMath.pow(10, mPower)));
|
||||||
|
|
||||||
set(value, fireChangeEvent);
|
set(value, fireChangeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frequency value.
|
||||||
|
* @return frequency value.
|
||||||
|
*/
|
||||||
public long getFrequency()
|
public long getFrequency()
|
||||||
{
|
{
|
||||||
return mValue * (long)FastMath.pow(10, mPower);
|
return mValue * (long)FastMath.pow(10, mPower);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the unit value and fires a change event
|
||||||
|
*/
|
||||||
public void increment()
|
public void increment()
|
||||||
{
|
{
|
||||||
increment(true);
|
increment(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the unit value and fires a change event
|
||||||
|
* @param fireChangeEvent true to fire an event
|
||||||
|
*/
|
||||||
public void increment(boolean fireChangeEvent)
|
public void increment(boolean fireChangeEvent)
|
||||||
{
|
{
|
||||||
if(isEnabled())
|
if(isEnabled())
|
||||||
@@ -313,11 +337,18 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements the unit value and fires a change event.
|
||||||
|
*/
|
||||||
public void decrement()
|
public void decrement()
|
||||||
{
|
{
|
||||||
decrement(true);
|
decrement(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements the unit value and optionally fires a change event.
|
||||||
|
* @param fireChangeEvent set to true to fire an event
|
||||||
|
*/
|
||||||
public void decrement(boolean fireChangeEvent)
|
public void decrement(boolean fireChangeEvent)
|
||||||
{
|
{
|
||||||
if(isEnabled())
|
if(isEnabled())
|
||||||
@@ -349,7 +380,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
while(mValue < 0)
|
while(mValue < 0)
|
||||||
{
|
{
|
||||||
mValue += 10;
|
mValue += 10;
|
||||||
|
|
||||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||||
|
|
||||||
if(nextHigherDigit != null)
|
if(nextHigherDigit != null)
|
||||||
@@ -363,7 +393,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
while(mValue > 9)
|
while(mValue > 9)
|
||||||
{
|
{
|
||||||
mValue -= 10;
|
mValue -= 10;
|
||||||
|
|
||||||
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
Digit nextHigherDigit = mDigits.get(mPower + 1);
|
||||||
|
|
||||||
if(nextHigherDigit != null)
|
if(nextHigherDigit != null)
|
||||||
@@ -403,10 +432,8 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(se instanceof InvalidFrequencyException)
|
if(se instanceof InvalidFrequencyException ife)
|
||||||
{
|
{
|
||||||
InvalidFrequencyException ife = (InvalidFrequencyException)se;
|
|
||||||
|
|
||||||
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
|
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
|
||||||
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
|
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
|
||||||
}
|
}
|
||||||
@@ -415,13 +442,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
|
|
||||||
|
|
||||||
setText(String.valueOf(mValue));
|
setText(String.valueOf(mValue));
|
||||||
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Listener implements KeyListener,
|
/**
|
||||||
MouseListener,
|
* Multiple Listener interface implementation
|
||||||
MouseWheelListener
|
*/
|
||||||
|
private class Listener implements KeyListener, MouseListener, MouseWheelListener
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void keyReleased(KeyEvent e)
|
public void keyReleased(KeyEvent e)
|
||||||
@@ -503,14 +530,9 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyPressed(KeyEvent e)
|
public void keyPressed(KeyEvent e) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyTyped(KeyEvent e)
|
public void keyTyped(KeyEvent e) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mouseClicked(MouseEvent e)
|
public void mouseClicked(MouseEvent e)
|
||||||
@@ -528,21 +550,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mousePressed(MouseEvent e)
|
public void mousePressed(MouseEvent e) {}
|
||||||
{
|
public void mouseReleased(MouseEvent e) {}
|
||||||
}
|
|
||||||
|
|
||||||
public void mouseReleased(MouseEvent e)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mouseEntered(MouseEvent e)
|
public void mouseEntered(MouseEvent e)
|
||||||
{
|
{
|
||||||
Digit.this.setBackground(mHighlightColor);
|
Digit.this.setBackground(mHighlightColor);
|
||||||
|
|
||||||
setCursor(mBlankCursor);
|
|
||||||
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,9 +564,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
|||||||
public void mouseExited(MouseEvent e)
|
public void mouseExited(MouseEvent e)
|
||||||
{
|
{
|
||||||
Digit.this.setBackground(Color.WHITE);
|
Digit.this.setBackground(Color.WHITE);
|
||||||
|
|
||||||
setCursor(Cursor.getDefaultCursor());
|
|
||||||
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,16 +19,12 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.gui.playlist.alias;
|
package io.github.dsheirer.gui.playlist.alias;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.controlsfx.control.ToggleSwitch;
|
|
||||||
|
|
||||||
import com.google.common.collect.Ordering;
|
import com.google.common.collect.Ordering;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.Alias;
|
import io.github.dsheirer.alias.Alias;
|
||||||
import io.github.dsheirer.gui.playlist.Editor;
|
import io.github.dsheirer.gui.playlist.Editor;
|
||||||
import io.github.dsheirer.icon.Icon;
|
import io.github.dsheirer.icon.Icon;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
|
import java.util.List;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
@@ -49,6 +45,7 @@ import javafx.scene.layout.ColumnConstraints;
|
|||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
|
import org.controlsfx.control.ToggleSwitch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for multiple selected aliases providing limited options for changing attributes of multiple aliases
|
* 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 ToggleSwitch mMonitorAudioToggleSwitch;
|
||||||
private ComboBox<Integer> mMonitorPriorityComboBox;
|
private ComboBox<Integer> mMonitorPriorityComboBox;
|
||||||
private Button mApplyMonitorButton;
|
private Button mApplyMonitorButton;
|
||||||
|
private ToggleSwitch mRecordToggleSwitch;
|
||||||
|
private Button mApplyRecordButton;
|
||||||
|
|
||||||
private BooleanProperty mChangeInProgressProperty;
|
private BooleanProperty mChangeInProgressProperty;
|
||||||
private ReadOnlyBooleanProperty mChangeInProgressROProperty;
|
private ReadOnlyBooleanProperty mChangeInProgressROProperty;
|
||||||
@@ -144,6 +143,17 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
|||||||
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
|
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
|
||||||
gridPane.getChildren().add(getApplyMonitorButton());
|
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);
|
getChildren().add(gridPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +336,19 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
|||||||
return mMonitorAudioToggleSwitch;
|
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()
|
private ComboBox<Integer> getMonitorPriorityComboBox()
|
||||||
{
|
{
|
||||||
if(mMonitorPriorityComboBox == null)
|
if(mMonitorPriorityComboBox == null)
|
||||||
@@ -346,8 +369,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
|||||||
return mMonitorPriorityComboBox;
|
return mMonitorPriorityComboBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int setCount = 0;
|
|
||||||
|
|
||||||
private Button getApplyMonitorButton()
|
private Button getApplyMonitorButton()
|
||||||
{
|
{
|
||||||
if(mApplyMonitorButton == null)
|
if(mApplyMonitorButton == null)
|
||||||
@@ -378,7 +399,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
|||||||
for(Alias alias : getItem())
|
for(Alias alias : getItem())
|
||||||
{
|
{
|
||||||
alias.setCallPriority(pri);
|
alias.setCallPriority(pri);
|
||||||
//System.out.println(++setCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endChange();
|
endChange();
|
||||||
@@ -389,6 +409,25 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
|||||||
return mApplyMonitorButton;
|
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
|
* Cell factory for combo box for dislaying icon name and graphic
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
idsColumn.setCellValueFactory(new IdentifierCountCell());
|
idsColumn.setCellValueFactory(new IdentifierCountCell());
|
||||||
|
|
||||||
TableColumn<Alias, Boolean> errorsColumn = new TableColumn<>("Error");
|
TableColumn<Alias, Boolean> errorsColumn = new TableColumn<>("Error");
|
||||||
errorsColumn.setPrefWidth(80);
|
errorsColumn.setPrefWidth(120);
|
||||||
errorsColumn.setCellValueFactory(new PropertyValueFactory<>("overlap"));
|
errorsColumn.setCellValueFactory(new PropertyValueFactory<>("overlap"));
|
||||||
errorsColumn.setCellFactory(param ->
|
errorsColumn.setCellFactory(param ->
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.gui.playlist.alias;
|
||||||
@@ -37,13 +34,16 @@ import io.github.dsheirer.alias.action.script.ScriptAction;
|
|||||||
import io.github.dsheirer.alias.id.AliasID;
|
import io.github.dsheirer.alias.id.AliasID;
|
||||||
import io.github.dsheirer.alias.id.AliasIDType;
|
import io.github.dsheirer.alias.id.AliasIDType;
|
||||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
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.esn.Esn;
|
||||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
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.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
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.Talkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||||
@@ -63,6 +63,11 @@ import io.github.dsheirer.preference.PreferenceType;
|
|||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
@@ -106,12 +111,6 @@ import org.controlsfx.control.ToggleSwitch;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for configuring individual aliases
|
* Editor for configuring individual aliases
|
||||||
*/
|
*/
|
||||||
@@ -365,7 +364,10 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
for(AliasID aliasID: getIdentifiersList().getItems())
|
for(AliasID aliasID: getIdentifiersList().getItems())
|
||||||
{
|
{
|
||||||
//Create a copy of the identifier so that the alias and the editor don't have the same instance
|
//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
|
//Remove and replace alias actions
|
||||||
@@ -684,8 +686,10 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
||||||
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
||||||
p25Menu.getItems().add(new AddTalkgroupRangeItem(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 AddRadioIdItem(Protocol.APCO25));
|
||||||
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
||||||
|
p25Menu.getItems().add(new AddP25FullyQualifiedRadioIdItem());
|
||||||
p25Menu.getItems().add(new SeparatorMenuItem());
|
p25Menu.getItems().add(new SeparatorMenuItem());
|
||||||
p25Menu.getItems().add(new AddUserStatusItem());
|
p25Menu.getItems().add(new AddUserStatusItem());
|
||||||
p25Menu.getItems().add(new AddUnitStatusItem());
|
p25Menu.getItems().add(new AddUnitStatusItem());
|
||||||
@@ -717,6 +721,7 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
|
Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
|
||||||
nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
|
nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
|
||||||
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
|
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
|
||||||
|
nbfmMenu.getItems().add(new AddDcsItem());
|
||||||
|
|
||||||
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
|
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
|
||||||
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
|
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
|
||||||
@@ -1281,6 +1286,24 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Digital Coded Squelch (DCS) alias identifier menu item
|
||||||
|
*/
|
||||||
|
public class AddDcsItem extends MenuItem
|
||||||
|
{
|
||||||
|
public AddDcsItem()
|
||||||
|
{
|
||||||
|
super("Digital Coded Squelch (DCS)");
|
||||||
|
setOnAction(event -> {
|
||||||
|
Dcs dcs = new Dcs();
|
||||||
|
getIdentifiersList().getItems().add(dcs);
|
||||||
|
getIdentifiersList().getSelectionModel().select(dcs);
|
||||||
|
getIdentifiersList().scrollTo(dcs);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu Item for adding a new protocol-specific Radio ID alias identifier
|
* Menu Item for adding a new protocol-specific Radio ID alias identifier
|
||||||
*/
|
*/
|
||||||
@@ -1303,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
|
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
|
||||||
*/
|
*/
|
||||||
@@ -1486,7 +1547,25 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
|
|
||||||
if(item != null)
|
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;
|
Talkgroup talkgroup = (Talkgroup)item;
|
||||||
Protocol protocol = talkgroup.getProtocol();
|
Protocol protocol = talkgroup.getProtocol();
|
||||||
@@ -1533,6 +1612,24 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
}
|
}
|
||||||
setText(sb.toString());
|
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)
|
else if(item instanceof Radio)
|
||||||
{
|
{
|
||||||
Radio radio = (Radio)item;
|
Radio radio = (Radio)item;
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.dcs.Dcs;
|
||||||
|
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor for Digital Coded Squelch (DCS) alias identifiers
|
||||||
|
*/
|
||||||
|
public class DcsEditor extends IdentifierEditor<Dcs>
|
||||||
|
{
|
||||||
|
private static final Logger mLog = LoggerFactory.getLogger(DcsEditor.class);
|
||||||
|
private ComboBox<DCSCode> mDCSCodeComboBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*/
|
||||||
|
public DcsEditor()
|
||||||
|
{
|
||||||
|
GridPane gridPane = new GridPane();
|
||||||
|
gridPane.setHgap(5);
|
||||||
|
|
||||||
|
Label typeLabel = new Label("DCS Code");
|
||||||
|
GridPane.setConstraints(typeLabel, 0, 0);
|
||||||
|
gridPane.getChildren().add(typeLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getDCSCodeComboBox(), 1, 0);
|
||||||
|
gridPane.getChildren().add(getDCSCodeComboBox());
|
||||||
|
|
||||||
|
getChildren().add(gridPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(Dcs item)
|
||||||
|
{
|
||||||
|
super.setItem(item);
|
||||||
|
if(item.isValid())
|
||||||
|
{
|
||||||
|
getDCSCodeComboBox().getSelectionModel().select(item.getDCSCode());
|
||||||
|
}
|
||||||
|
modifiedProperty().set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combo-box loaded with DCS codes
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private ComboBox<DCSCode> getDCSCodeComboBox()
|
||||||
|
{
|
||||||
|
if(mDCSCodeComboBox == null)
|
||||||
|
{
|
||||||
|
mDCSCodeComboBox = new ComboBox<>();
|
||||||
|
mDCSCodeComboBox.getItems().addAll(DCSCode.STANDARD_CODES);
|
||||||
|
mDCSCodeComboBox.getItems().addAll(DCSCode.INVERTED_CODES);
|
||||||
|
mDCSCodeComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
getItem().setDCSCode(getDCSCodeComboBox().getSelectionModel().getSelectedItem());
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDCSCodeComboBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -35,10 +35,16 @@ public class IdentifierEditorFactory
|
|||||||
{
|
{
|
||||||
switch(type)
|
switch(type)
|
||||||
{
|
{
|
||||||
|
case DCS:
|
||||||
|
return new DcsEditor();
|
||||||
case ESN:
|
case ESN:
|
||||||
return new EsnEditor();
|
return new EsnEditor();
|
||||||
case LOJACK:
|
case LOJACK:
|
||||||
return new LojackEditor();
|
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:
|
case RADIO_ID:
|
||||||
return new RadioIdEditor(userPreferences);
|
return new RadioIdEditor(userPreferences);
|
||||||
case RADIO_ID_RANGE:
|
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
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public AMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -38,6 +38,8 @@ import io.github.dsheirer.source.config.SourceConfigTuner;
|
|||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
@@ -69,9 +71,6 @@ import org.controlsfx.control.ToggleSwitch;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel configuration editor
|
* Channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -100,6 +99,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
|||||||
private IconNode mPlayGraphicNode;
|
private IconNode mPlayGraphicNode;
|
||||||
private IconNode mStopGraphicNode;
|
private IconNode mStopGraphicNode;
|
||||||
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
|
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
|
||||||
|
private IFilterProcessor mFilterProcessor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
@@ -108,11 +108,12 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
|||||||
* @param userPreferences for preferences
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public ChannelConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
public ChannelConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||||
UserPreferences userPreferences)
|
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||||
{
|
{
|
||||||
mPlaylistManager = playlistManager;
|
mPlaylistManager = playlistManager;
|
||||||
mTunerManager = tunerManager;
|
mTunerManager = tunerManager;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
|
mFilterProcessor = filterProcessor;
|
||||||
|
|
||||||
setMaxWidth(Double.MAX_VALUE);
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
@@ -689,7 +690,16 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
|||||||
mSaveButton.setMaxWidth(Double.MAX_VALUE);
|
mSaveButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
mSaveButton.disableProperty().bind(modifiedProperty().not());
|
mSaveButton.disableProperty().bind(modifiedProperty().not());
|
||||||
mSaveButton.setOnAction(event -> {
|
mSaveButton.setOnAction(event -> {
|
||||||
|
if(mFilterProcessor != null)
|
||||||
|
{
|
||||||
|
mFilterProcessor.clearFilter();
|
||||||
save();
|
save();
|
||||||
|
mFilterProcessor.restoreFilter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
if(getItem().isProcessing())
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -23,11 +23,10 @@ import io.github.dsheirer.module.decode.DecoderType;
|
|||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to channel configuration editors for various decoder types.
|
* 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
|
* Constructs an editor for the specified decoder type
|
||||||
* @param decoderType to create
|
* @param decoderType to create
|
||||||
* @param playlistManager for the editor
|
* @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
|
* @return constructed editor
|
||||||
*/
|
*/
|
||||||
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
|
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
|
||||||
TunerManager tunerManager, UserPreferences userPreferences)
|
TunerManager tunerManager, UserPreferences userPreferences,
|
||||||
|
IFilterProcessor filterProcessor)
|
||||||
{
|
{
|
||||||
switch(decoderType)
|
switch(decoderType)
|
||||||
{
|
{
|
||||||
case AM:
|
case AM:
|
||||||
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case DMR:
|
case DMR:
|
||||||
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case NBFM:
|
case NBFM:
|
||||||
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case LTR_NET:
|
case LTR_NET:
|
||||||
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case LTR:
|
case LTR:
|
||||||
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case MPT1327:
|
case MPT1327:
|
||||||
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case PASSPORT:
|
case PASSPORT:
|
||||||
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case P25_PHASE1:
|
case P25_PHASE1:
|
||||||
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case P25_PHASE2:
|
case P25_PHASE2:
|
||||||
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences);
|
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
default:
|
default:
|
||||||
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -26,6 +26,12 @@ import io.github.dsheirer.module.decode.DecoderType;
|
|||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
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.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
@@ -63,17 +69,10 @@ import org.controlsfx.control.textfield.TextFields;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.
|
* 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 final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
|
||||||
private PlaylistManager mPlaylistManager;
|
private PlaylistManager mPlaylistManager;
|
||||||
@@ -106,7 +105,8 @@ public class ChannelEditor extends SplitPane
|
|||||||
mPlaylistManager = playlistManager;
|
mPlaylistManager = playlistManager;
|
||||||
mTunerManager = tunerManager;
|
mTunerManager = tunerManager;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager, userPreferences);
|
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
|
||||||
|
userPreferences, this);
|
||||||
|
|
||||||
HBox channelsBox = new HBox();
|
HBox channelsBox = new HBox();
|
||||||
channelsBox.setSpacing(10.0);
|
channelsBox.setSpacing(10.0);
|
||||||
@@ -203,7 +203,7 @@ public class ChannelEditor extends SplitPane
|
|||||||
if(editor == null)
|
if(editor == null)
|
||||||
{
|
{
|
||||||
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
|
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
|
||||||
mTunerManager, mUserPreferences);
|
mTunerManager, mUserPreferences, this);
|
||||||
|
|
||||||
if(editor != null)
|
if(editor != null)
|
||||||
{
|
{
|
||||||
@@ -374,6 +374,24 @@ public class ChannelEditor extends SplitPane
|
|||||||
mChannelFilteredList.setPredicate(mChannelListFilter);
|
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()
|
private TableView<Channel> getChannelTableView()
|
||||||
{
|
{
|
||||||
if(mChannelTableView == null)
|
if(mChannelTableView == null)
|
||||||
@@ -449,6 +467,25 @@ public class ChannelEditor extends SplitPane
|
|||||||
nameColumn.setPrefWidth(200);
|
nameColumn.setPrefWidth(200);
|
||||||
|
|
||||||
TableColumn frequencyColumn = new TableColumn("Frequency");
|
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.setCellValueFactory(new FrequencyCellValueFactory());
|
||||||
frequencyColumn.setPrefWidth(100);
|
frequencyColumn.setPrefWidth(100);
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
private ToggleSwitch mUseCompressedTalkgroupsToggle;
|
private ToggleSwitch mUseCompressedTalkgroupsToggle;
|
||||||
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
||||||
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
|
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
|
||||||
private IntegerTextField mLogicalSlotNumberField;
|
private IntegerTextField mLogicalChannelNumberField;
|
||||||
private FrequencyField mDownlinkFrequencyField;
|
private FrequencyField mDownlinkFrequencyField;
|
||||||
// private FrequencyField mUplinkFrequencyField;
|
// private FrequencyField mUplinkFrequencyField;
|
||||||
private Button mAddTimeslotFrequencyButton;
|
private Button mAddTimeslotFrequencyButton;
|
||||||
@@ -99,9 +99,9 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences for preferences
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public DMRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||||
@@ -172,7 +172,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row);
|
GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row);
|
||||||
gridPane.getChildren().add(useCompressedTalkgroupsLabel);
|
gridPane.getChildren().add(useCompressedTalkgroupsLabel);
|
||||||
|
|
||||||
Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies");
|
Label timeslotTableLabel = new Label("Logical Channel Number (LCN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies. LSN = Logical Slot Number");
|
||||||
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
|
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
|
||||||
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
|
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
|
||||||
gridPane.getChildren().add(timeslotTableLabel);
|
gridPane.getChildren().add(timeslotTableLabel);
|
||||||
@@ -194,17 +194,13 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
editorBox.setAlignment(Pos.CENTER_LEFT);
|
editorBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
editorBox.setSpacing(5);
|
editorBox.setSpacing(5);
|
||||||
|
|
||||||
Label lsnLabel = new Label("LSN");
|
Label lcnLabel = new Label("LCN");
|
||||||
editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField());
|
editorBox.getChildren().addAll(lcnLabel, getLogicalChannelNumberField());
|
||||||
|
|
||||||
Label downlinkLabel = new Label("Frequency (MHz)");
|
Label downlinkLabel = new Label("Frequency (MHz)");
|
||||||
downlinkLabel.setPadding(new Insets(0,0,0,5));
|
downlinkLabel.setPadding(new Insets(0,0,0,5));
|
||||||
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
|
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
|
||||||
|
|
||||||
// Label uplinkLabel = new Label("Uplink");
|
|
||||||
// uplinkLabel.setPadding(new Insets(0,0,0,5));
|
|
||||||
// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField());
|
|
||||||
|
|
||||||
GridPane.setConstraints(editorBox, 0, row, 4, 1);
|
GridPane.setConstraints(editorBox, 0, row, 4, 1);
|
||||||
gridPane.getChildren().add(editorBox);
|
gridPane.getChildren().add(editorBox);
|
||||||
|
|
||||||
@@ -287,20 +283,22 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
|
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
|
||||||
mTimeslotFrequencyTable.setPrefHeight(100.0);
|
mTimeslotFrequencyTable.setPrefHeight(100.0);
|
||||||
|
|
||||||
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LSN");
|
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LCN");
|
||||||
numberColumn.setPrefWidth(70);
|
numberColumn.setPrefWidth(75);
|
||||||
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
|
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
|
||||||
mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
|
mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
|
||||||
mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
|
mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
|
||||||
|
|
||||||
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
|
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
|
||||||
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
|
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
|
||||||
downlinkColumn.setPrefWidth(175);
|
downlinkColumn.setPrefWidth(150);
|
||||||
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
|
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
|
||||||
|
|
||||||
// TableColumn<TimeslotFrequency,Number> uplinkColumn = new TableColumn("Uplink (MHz)");
|
TableColumn<TimeslotFrequency,String> lsnColumn = new TableColumn("IDs (TS1/TS2)");
|
||||||
// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz());
|
lsnColumn.setPrefWidth(225);
|
||||||
// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn);
|
lsnColumn.setCellValueFactory(new PropertyValueFactory<>("logicalSlotNumbers"));
|
||||||
|
mTimeslotFrequencyTable.getColumns().addAll(lsnColumn);
|
||||||
|
|
||||||
|
|
||||||
mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
|
mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
|
.addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
|
||||||
@@ -317,20 +315,20 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
//Preserve the current modified flag state since setting values in the editor will change it.
|
//Preserve the current modified flag state since setting values in the editor will change it.
|
||||||
boolean modified = modifiedProperty().get();
|
boolean modified = modifiedProperty().get();
|
||||||
|
|
||||||
getLogicalSlotNumberField().setDisable(timeslot == null);
|
getLogicalChannelNumberField().setDisable(timeslot == null);
|
||||||
getDownlinkFrequencyField().setDisable(timeslot == null);
|
getDownlinkFrequencyField().setDisable(timeslot == null);
|
||||||
// getUplinkFrequencyField().setDisable(timeslot == null);
|
// getUplinkFrequencyField().setDisable(timeslot == null);
|
||||||
getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
|
getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
|
||||||
|
|
||||||
if(timeslot != null)
|
if(timeslot != null)
|
||||||
{
|
{
|
||||||
getLogicalSlotNumberField().set(timeslot.getNumber());
|
getLogicalChannelNumberField().set(timeslot.getNumber());
|
||||||
getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
|
getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
|
||||||
// getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
|
// getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
getLogicalSlotNumberField().set(0);
|
getLogicalChannelNumberField().set(0);
|
||||||
getDownlinkFrequencyField().set(0);
|
getDownlinkFrequencyField().set(0);
|
||||||
// getUplinkFrequencyField().set(0);
|
// getUplinkFrequencyField().set(0);
|
||||||
}
|
}
|
||||||
@@ -412,19 +410,19 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
return mDeleteTimeslotFrequencyButton;
|
return mDeleteTimeslotFrequencyButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntegerTextField getLogicalSlotNumberField()
|
private IntegerTextField getLogicalChannelNumberField()
|
||||||
{
|
{
|
||||||
if(mLogicalSlotNumberField == null)
|
if(mLogicalChannelNumberField == null)
|
||||||
{
|
{
|
||||||
mLogicalSlotNumberField = new IntegerTextField();
|
mLogicalChannelNumberField = new IntegerTextField();
|
||||||
mLogicalSlotNumberField.setDisable(true);
|
mLogicalChannelNumberField.setDisable(true);
|
||||||
mLogicalSlotNumberField.setPrefWidth(65);
|
mLogicalChannelNumberField.setPrefWidth(65);
|
||||||
mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
|
mLogicalChannelNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
|
TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
|
||||||
|
|
||||||
if(selected != null)
|
if(selected != null)
|
||||||
{
|
{
|
||||||
Integer value = mLogicalSlotNumberField.get();
|
Integer value = mLogicalChannelNumberField.get();
|
||||||
|
|
||||||
if(value != null)
|
if(value != null)
|
||||||
{
|
{
|
||||||
@@ -436,7 +434,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return mLogicalSlotNumberField;
|
return mLogicalChannelNumberField;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FrequencyField getDownlinkFrequencyField()
|
private FrequencyField getDownlinkFrequencyField()
|
||||||
@@ -610,8 +608,8 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
getTimeslotTable().setDisable(config == null);
|
getTimeslotTable().setDisable(config == null);
|
||||||
getAddTimeslotFrequencyButton().setDisable(config == null);
|
getAddTimeslotFrequencyButton().setDisable(config == null);
|
||||||
getDeleteTimeslotFrequencyButton().setDisable(true);
|
getDeleteTimeslotFrequencyButton().setDisable(true);
|
||||||
getLogicalSlotNumberField().set(0);
|
getLogicalChannelNumberField().set(0);
|
||||||
getLogicalSlotNumberField().setDisable(true);
|
getLogicalChannelNumberField().setDisable(true);
|
||||||
getDownlinkFrequencyField().set(0);
|
getDownlinkFrequencyField().set(0);
|
||||||
getDownlinkFrequencyField().setDisable(true);
|
getDownlinkFrequencyField().setDisable(true);
|
||||||
// getUplinkFrequencyField().set(0);
|
// getUplinkFrequencyField().set(0);
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LTR (standard) channel configuration editor
|
* LTR (standard) channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -77,9 +76,9 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences for preferences
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public LTRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LTR-Net channel configuration editor
|
* LTR-Net channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -76,9 +75,10 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param tunerManager for tuners
|
* @param tunerManager for tuners
|
||||||
* @param userPreferences for preferences
|
* @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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -38,6 +38,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
@@ -56,9 +58,6 @@ import javafx.scene.layout.VBox;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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_MAXIMUM;
|
||||||
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MINIMUM;
|
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
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
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.FrequencyEditor;
|
||||||
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
|
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
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.AuxDecodeConfiguration;
|
||||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
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.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
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.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
@@ -58,9 +59,6 @@ import javafx.scene.text.TextAlignment;
|
|||||||
import org.controlsfx.control.SegmentedButton;
|
import org.controlsfx.control.SegmentedButton;
|
||||||
import org.controlsfx.control.ToggleSwitch;
|
import org.controlsfx.control.ToggleSwitch;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Narrow-Band FM channel configuration editor
|
* Narrow-Band FM channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -95,9 +93,9 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences for preferences
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public NBFMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
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.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
@@ -54,9 +56,6 @@ import org.controlsfx.control.ToggleSwitch;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P25 Phase 1 channel configuration editor
|
* P25 Phase 1 channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -83,9 +82,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences for preferences
|
* @param userPreferences for preferences
|
||||||
*/
|
*/
|
||||||
public P25P1ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -46,9 +48,6 @@ import javafx.scene.layout.VBox;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P25 Phase 2 channel configuration editor
|
* P25 Phase 2 channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -72,9 +71,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences
|
* @param userPreferences
|
||||||
*/
|
*/
|
||||||
public P25P2ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
|
|||||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TitledPane;
|
import javafx.scene.control.TitledPane;
|
||||||
@@ -43,9 +45,6 @@ import javafx.scene.layout.VBox;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passport channel configuration editor
|
* Passport channel configuration editor
|
||||||
*/
|
*/
|
||||||
@@ -68,9 +67,9 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param userPreferences
|
* @param userPreferences
|
||||||
*/
|
*/
|
||||||
public PassportConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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(getSourcePane());
|
||||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -45,9 +45,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
|
|||||||
* @param aliasModel
|
* @param aliasModel
|
||||||
*/
|
*/
|
||||||
public UnknownConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
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());
|
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
|
|||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.rrapi.type.Agency;
|
import io.github.dsheirer.rrapi.type.Agency;
|
||||||
import io.github.dsheirer.rrapi.type.AgencyInfo;
|
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.service.radioreference.RadioReference;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
@@ -158,9 +159,14 @@ public class AgencyEditor extends VBox
|
|||||||
ThreadPool.CACHED.submit(() -> {
|
ThreadPool.CACHED.submit(() -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
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);
|
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
{
|
{
|
||||||
mLog.error("Error retrieving agency info", t);
|
mLog.error("Error retrieving agency info", t);
|
||||||
@@ -196,11 +202,11 @@ public class AgencyEditor extends VBox
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
else if(o1.getName() == null)
|
else if(o1.getName() == null || o2 instanceof CountyAgency)
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
else if(o2.getName() == null)
|
else if(o2.getName() == null || o1 instanceof CountyAgency)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,9 +158,12 @@ public class AgencyFrequencyEditor extends GridPane
|
|||||||
|
|
||||||
if(categories != null && !categories.isEmpty())
|
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().getItems().addAll(categories);
|
||||||
getCategoryComboBox().getSelectionModel().select(ALL_CATEGORIES);
|
getCategoryComboBox().getSelectionModel().select(categories.get(0));
|
||||||
}
|
}
|
||||||
else
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -41,13 +41,12 @@ import io.github.dsheirer.rrapi.type.Tag;
|
|||||||
import io.github.dsheirer.rrapi.type.Talkgroup;
|
import io.github.dsheirer.rrapi.type.Talkgroup;
|
||||||
import io.github.dsheirer.rrapi.type.Type;
|
import io.github.dsheirer.rrapi.type.Type;
|
||||||
import io.github.dsheirer.rrapi.type.Voice;
|
import io.github.dsheirer.rrapi.type.Voice;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences.
|
* Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences.
|
||||||
@@ -374,16 +373,10 @@ public class RadioReferenceDecoder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int lsn = (lcn - 1) * 2 + 1;
|
TimeslotFrequency timeslotFrequency = new TimeslotFrequency();
|
||||||
TimeslotFrequency timeslot1Frequency = new TimeslotFrequency();
|
timeslotFrequency.setNumber(lcn);
|
||||||
timeslot1Frequency.setNumber(lsn);
|
timeslotFrequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
||||||
timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
frequencies.add(timeslotFrequency);
|
||||||
frequencies.add(timeslot1Frequency);
|
|
||||||
|
|
||||||
TimeslotFrequency timeslot2Frequency = new TimeslotFrequency();
|
|
||||||
timeslot2Frequency.setNumber(lsn + 1);
|
|
||||||
timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
|
||||||
frequencies.add(timeslot2Frequency);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return frequencies;
|
return frequencies;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.radioreference;
|
|||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.rrapi.RadioReferenceException;
|
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.AuthorizationInformation;
|
||||||
import io.github.dsheirer.rrapi.type.Country;
|
import io.github.dsheirer.rrapi.type.Country;
|
||||||
import io.github.dsheirer.rrapi.type.CountryInfo;
|
import io.github.dsheirer.rrapi.type.CountryInfo;
|
||||||
@@ -55,6 +56,7 @@ import jiconfont.javafx.IconNode;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -689,7 +691,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer<Authori
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
getCountySystemEditor().setSystems(countyInfo.getSystems());
|
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)
|
catch(RadioReferenceException rre)
|
||||||
|
|||||||
@@ -294,7 +294,11 @@ public class SystemEditor extends VBox
|
|||||||
{
|
{
|
||||||
CountyInfo countyInfo = null;
|
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());
|
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public class StreamEditorFactory
|
|||||||
return new BroadcastifyStreamEditor(playlistManager);
|
return new BroadcastifyStreamEditor(playlistManager);
|
||||||
case RDIOSCANNER_CALL:
|
case RDIOSCANNER_CALL:
|
||||||
return new RdioScannerEditor(playlistManager);
|
return new RdioScannerEditor(playlistManager);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzEditor(playlistManager);
|
||||||
case BROADCASTIFY_CALL:
|
case BROADCASTIFY_CALL:
|
||||||
return new BroadcastifyCallEditor(playlistManager);
|
return new BroadcastifyCallEditor(playlistManager);
|
||||||
case ICECAST_HTTP:
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -28,6 +28,11 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
|
|||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
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.application.Platform;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
@@ -48,6 +53,7 @@ import javafx.scene.control.TabPane;
|
|||||||
import javafx.scene.control.TableCell;
|
import javafx.scene.control.TableCell;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -58,12 +64,6 @@ import jiconfont.javafx.IconNode;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* Editor for broadcast audio stream configurations
|
||||||
*/
|
*/
|
||||||
@@ -75,6 +75,7 @@ public class StreamingEditor extends SplitPane
|
|||||||
private TableView<ConfiguredBroadcast> mConfiguredBroadcastTableView;
|
private TableView<ConfiguredBroadcast> mConfiguredBroadcastTableView;
|
||||||
private MenuButton mNewButton;
|
private MenuButton mNewButton;
|
||||||
private Button mDeleteButton;
|
private Button mDeleteButton;
|
||||||
|
private Button mRefreshButton;
|
||||||
private TabPane mTabPane;
|
private TabPane mTabPane;
|
||||||
private Tab mConfigurationTab;
|
private Tab mConfigurationTab;
|
||||||
private Tab mAliasTab;
|
private Tab mAliasTab;
|
||||||
@@ -101,7 +102,7 @@ public class StreamingEditor extends SplitPane
|
|||||||
refreshBroadcastifyStreams();
|
refreshBroadcastifyStreams();
|
||||||
|
|
||||||
VBox buttonsBox = new VBox();
|
VBox buttonsBox = new VBox();
|
||||||
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton());
|
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton(), getRefreshButton());
|
||||||
buttonsBox.setPadding(new Insets(0, 0, 0, 10));
|
buttonsBox.setPadding(new Insets(0, 0, 0, 10));
|
||||||
buttonsBox.setSpacing(10);
|
buttonsBox.setSpacing(10);
|
||||||
|
|
||||||
@@ -231,11 +232,7 @@ public class StreamingEditor extends SplitPane
|
|||||||
{
|
{
|
||||||
if(mPlaylistManager.getRadioReference().availableProperty().get())
|
if(mPlaylistManager.getRadioReference().availableProperty().get())
|
||||||
{
|
{
|
||||||
ThreadPool.CACHED.submit(new Runnable()
|
ThreadPool.CACHED.submit(() -> {
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
|
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
|
||||||
@@ -246,7 +243,6 @@ public class StreamingEditor extends SplitPane
|
|||||||
{
|
{
|
||||||
mLog.error("Unable to refresh broadcastify stream configuration(s)");
|
mLog.error("Unable to refresh broadcastify stream configuration(s)");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,6 +344,22 @@ public class StreamingEditor extends SplitPane
|
|||||||
return mNewButton;
|
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()
|
private Button getDeleteButton()
|
||||||
{
|
{
|
||||||
if(mDeleteButton == null)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
|
|||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
||||||
|
import java.io.File;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
@@ -31,6 +32,7 @@ import javafx.geometry.Orientation;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Separator;
|
import javafx.scene.control.Separator;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -39,8 +41,6 @@ import javafx.stage.Stage;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference settings for channel event view
|
* Preference settings for channel event view
|
||||||
@@ -97,6 +97,9 @@ public class DirectoryPreferenceEditor extends HBox
|
|||||||
private Button mResetStreamingButton;
|
private Button mResetStreamingButton;
|
||||||
private Label mStreamingPathLabel;
|
private Label mStreamingPathLabel;
|
||||||
|
|
||||||
|
private Spinner<Integer> mRecordingSpinner;
|
||||||
|
private Spinner<Integer> mEventLogSpinner;
|
||||||
|
|
||||||
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
||||||
@@ -247,11 +250,57 @@ public class DirectoryPreferenceEditor extends HBox
|
|||||||
|
|
||||||
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
||||||
mEditorPane.add(getResetStreamingButton(), 3, row++);
|
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;
|
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()
|
private Label getApplicationRootLabel()
|
||||||
{
|
{
|
||||||
if(mApplicationRootLabel == null)
|
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.message.IMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
|
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
|
||||||
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
|
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
|
||||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||||
import io.github.dsheirer.record.binary.BinaryReader;
|
import io.github.dsheirer.record.binary.BinaryReader;
|
||||||
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
@@ -40,10 +42,10 @@ import javafx.collections.transformation.FilteredList;
|
|||||||
import javafx.collections.transformation.SortedList;
|
import javafx.collections.transformation.SortedList;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TablePosition;
|
import javafx.scene.control.TablePosition;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
@@ -58,21 +60,19 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
|
||||||
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory";
|
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr";
|
||||||
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class);
|
private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class);
|
||||||
private Scene mScene;
|
|
||||||
private Button mSelectFileButton;
|
private Button mSelectFileButton;
|
||||||
private Label mSelectedFileLabel;
|
private Label mSelectedFileLabel;
|
||||||
private TableView<IMessage> mMessageTableView;
|
private TableView<IMessage> mMessageTableView;
|
||||||
@@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox
|
|||||||
private TextField mFindText;
|
private TextField mFindText;
|
||||||
private Button mFindButton;
|
private Button mFindButton;
|
||||||
private Button mFindNextButton;
|
private Button mFindNextButton;
|
||||||
|
private ProgressIndicator mLoadingIndicator;
|
||||||
|
|
||||||
public DMRRecordingViewer()
|
public DmrViewer()
|
||||||
{
|
{
|
||||||
setPadding(new Insets(5));
|
setPadding(new Insets(5));
|
||||||
setSpacing(5);
|
setSpacing(5);
|
||||||
@@ -132,6 +133,21 @@ public class DMRRecordingViewer extends VBox
|
|||||||
getChildren().addAll(fileBox, filterBox, getMessageTableView());
|
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
|
* Processes the recording file and loads the content into the viewer
|
||||||
* @param file containing a .bits recording of decoded DMR data.
|
* @param file containing a .bits recording of decoded DMR data.
|
||||||
@@ -140,18 +156,23 @@ public class DMRRecordingViewer extends VBox
|
|||||||
{
|
{
|
||||||
if(file != null && file.exists())
|
if(file != null && file.exists())
|
||||||
{
|
{
|
||||||
getSelectedFileLabel().setText(file.getName());
|
|
||||||
mMessages.clear();
|
mMessages.clear();
|
||||||
|
getLoadingIndicator().setVisible(true);
|
||||||
|
getSelectedFileLabel().setText("Loading ...");
|
||||||
|
final boolean useCompressed = getUseCompressedTalkgroups().isSelected();
|
||||||
|
|
||||||
|
ThreadPool.CACHED.submit(new Runnable()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
List<IMessage> messages = new ArrayList<>();
|
||||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||||
DecodeConfigDMR config = new DecodeConfigDMR();
|
DecodeConfigDMR config = new DecodeConfigDMR();
|
||||||
if(getUseCompressedTalkgroups().isSelected())
|
config.setUseCompressedTalkgroups(useCompressed);
|
||||||
{
|
|
||||||
config.setUseCompressedTalkgroups(true);
|
|
||||||
}
|
|
||||||
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
||||||
messageFramer.setListener(messageProcessor);
|
messageFramer.setListener(messageProcessor);
|
||||||
messageProcessor.setMessageListener(message -> mMessages.add(message));
|
messageProcessor.setMessageListener(message -> messages.add(message));
|
||||||
|
|
||||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||||
{
|
{
|
||||||
@@ -166,7 +187,14 @@ public class DMRRecordingViewer extends VBox
|
|||||||
ioe.printStackTrace();
|
ioe.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
getLoadingIndicator().setVisible(false);
|
||||||
|
getSelectedFileLabel().setText(file.getName());
|
||||||
|
mMessages.addAll(messages);
|
||||||
getMessageTableView().scrollTo(0);
|
getMessageTableView().scrollTo(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +284,7 @@ public class DMRRecordingViewer extends VBox
|
|||||||
if(mMessageTableView == null)
|
if(mMessageTableView == null)
|
||||||
{
|
{
|
||||||
mMessageTableView = new TableView<>();
|
mMessageTableView = new TableView<>();
|
||||||
|
mMessageTableView.setPlaceholder(getLoadingIndicator());
|
||||||
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
||||||
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
||||||
mMessageTableView.setItems(sortedList);
|
mMessageTableView.setItems(sortedList);
|
||||||
@@ -459,6 +488,12 @@ public class DMRRecordingViewer extends VBox
|
|||||||
if(mFindText == null)
|
if(mFindText == null)
|
||||||
{
|
{
|
||||||
mFindText = new TextField();
|
mFindText = new TextField();
|
||||||
|
mFindText.setOnKeyPressed(event -> {
|
||||||
|
if(event.getCode().equals(KeyCode.ENTER))
|
||||||
|
{
|
||||||
|
getFindButton().fire();
|
||||||
|
}
|
||||||
|
});
|
||||||
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
|
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,37 +538,9 @@ public class DMRRecordingViewer extends VBox
|
|||||||
{
|
{
|
||||||
if(mUseCompressedTalkgroups == null)
|
if(mUseCompressedTalkgroups == null)
|
||||||
{
|
{
|
||||||
mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups");
|
mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mUseCompressedTalkgroups;
|
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;
|
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
|
* Constructs an instance
|
||||||
*/
|
*/
|
||||||
public ViewDmrRecordingViewerRequest() {}
|
public ViewRecordingViewerRequest() {}
|
||||||
}
|
}
|
||||||
@@ -32,11 +32,11 @@ public enum Form
|
|||||||
CHANNEL_DESCRIPTOR,
|
CHANNEL_DESCRIPTOR,
|
||||||
CHANNEL_NAME,
|
CHANNEL_NAME,
|
||||||
CHANNEL_FREQUENCY,
|
CHANNEL_FREQUENCY,
|
||||||
|
DCS,
|
||||||
DECODER_TYPE,
|
DECODER_TYPE,
|
||||||
DTMF,
|
DTMF,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
ESN,
|
ESN,
|
||||||
FULLY_QUALIFIED_IDENTIFIER,
|
|
||||||
IPV4_ADDRESS,
|
IPV4_ADDRESS,
|
||||||
KNOX_TONE,
|
KNOX_TONE,
|
||||||
LOCATION,
|
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
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.identifier.patch;
|
||||||
|
|
||||||
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary grouping or regrouping of talkgroups or individual radios.
|
||||||
|
*/
|
||||||
public class PatchGroup
|
public class PatchGroup
|
||||||
{
|
{
|
||||||
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
|
|
||||||
|
|
||||||
private TalkgroupIdentifier mPatchGroupIdentifier;
|
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;
|
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()
|
public TalkgroupIdentifier getPatchGroup()
|
||||||
{
|
{
|
||||||
return mPatchGroupIdentifier;
|
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()
|
||||||
{
|
{
|
||||||
mPatchedGroupIdentifiers.add(patchedGroupIdentifier);
|
return !getPatchedRadioIdentifiers().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the talkgroup identifier to this patched group.
|
||||||
|
* @param patchedGroupIdentifier to add
|
||||||
|
*/
|
||||||
|
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
|
||||||
|
{
|
||||||
|
if(!mPatchedTalkgroupIdentifiers.contains(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)
|
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
|
@Override
|
||||||
@@ -72,9 +164,14 @@ public class PatchGroup
|
|||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("P:").append(getPatchGroup());
|
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();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -22,27 +22,23 @@ package io.github.dsheirer.identifier.patch;
|
|||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.IdentifierClass;
|
import io.github.dsheirer.identifier.IdentifierClass;
|
||||||
import io.github.dsheirer.identifier.Role;
|
import io.github.dsheirer.identifier.Role;
|
||||||
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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
|
* Traffic channel calls will reference a patch group by simply using the patch group identifier. This
|
||||||
* and maintains a map of the current patch groups.
|
* 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
|
||||||
* Traffic channel calls will sometimes reference a patch group by simply using the patch group identifier. This
|
* patch group reference on the control or traffic channel.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
public class PatchGroupManager
|
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<>();
|
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,13 +54,13 @@ public class PatchGroupManager
|
|||||||
public void clear()
|
public void clear()
|
||||||
{
|
{
|
||||||
mPatchGroupMap.clear();
|
mPatchGroupMap.clear();
|
||||||
mPatchGroupLastUpdatedMap.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the patch group identifier to the set of managed patch groups. If the patch group is already being
|
* 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, the new patch group argument is checked for additional patched talkgroups and they are added to the
|
||||||
* managed patch group.
|
* 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
|
* @param patchGroupIdentifier to add or update
|
||||||
*/
|
*/
|
||||||
@@ -76,8 +72,19 @@ public class PatchGroupManager
|
|||||||
{
|
{
|
||||||
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
|
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
|
||||||
{
|
{
|
||||||
PatchGroup current = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
|
PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
|
||||||
current.addPatchedGroups(update.getPatchedGroupIdentifiers());
|
|
||||||
|
//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
|
else
|
||||||
{
|
{
|
||||||
@@ -109,7 +116,6 @@ public class PatchGroupManager
|
|||||||
{
|
{
|
||||||
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
|
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
|
||||||
mPatchGroupMap.remove(id);
|
mPatchGroupMap.remove(id);
|
||||||
mPatchGroupLastUpdatedMap.remove(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,23 +159,28 @@ public class PatchGroupManager
|
|||||||
*/
|
*/
|
||||||
public Identifier update(Identifier identifier)
|
public Identifier update(Identifier identifier)
|
||||||
{
|
{
|
||||||
if(identifier == null)
|
if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
|
||||||
{
|
|
||||||
return identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
|
|
||||||
{
|
{
|
||||||
switch(identifier.getForm())
|
switch(identifier.getForm())
|
||||||
{
|
{
|
||||||
case PATCH_GROUP:
|
case TALKGROUP:
|
||||||
if(identifier instanceof PatchGroupIdentifier)
|
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();
|
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
|
||||||
|
|
||||||
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
|
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
|
||||||
if (mapValue != null) {
|
if (mapValue != null)
|
||||||
|
{
|
||||||
return mapValue;
|
return mapValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
|
|||||||
{
|
{
|
||||||
BW_3_0("3.0 kHz", 3000.0),
|
BW_3_0("3.0 kHz", 3000.0),
|
||||||
BW_5_0("5.0 kHz", 5000.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_8_33("8.33 kHz", 8333.0),
|
||||||
BW_12_5("12.5 kHz", 12500.0),
|
BW_12_5("12.5 kHz", 12500.0),
|
||||||
BW_15_0("15.0 kHz", 15000.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);
|
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
|
//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.
|
* Indicates if this entry is valid for the AM decoder.
|
||||||
|
|||||||
@@ -213,7 +213,12 @@ public enum DCSCode
|
|||||||
/**
|
/**
|
||||||
* Inverted DCS codes
|
* Inverted DCS codes
|
||||||
*/
|
*/
|
||||||
private static final EnumSet<DCSCode> INVERTED_CODES = EnumSet.range(I023, I754);
|
public static final EnumSet<DCSCode> INVERTED_CODES = EnumSet.range(I023, I754);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard DCS codes
|
||||||
|
*/
|
||||||
|
public static final EnumSet<DCSCode> STANDARD_CODES = EnumSet.range(N023,N754);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lookup map for quickly finding a DCS code from the transmitted value.
|
* Lookup map for quickly finding a DCS code from the transmitted value.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class DCSMessage extends Message
|
|||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.name();
|
return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -284,11 +284,27 @@ public class DMRBurstFramer implements Listener<Dibit>
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
|
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
|
||||||
//don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
|
//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)
|
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;
|
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -298,7 +314,21 @@ public class DMRBurstFramer implements Listener<Dibit>
|
|||||||
|
|
||||||
if(burst2 != null)
|
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;
|
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
|||||||
import io.github.dsheirer.source.ISourceEventListener;
|
import io.github.dsheirer.source.ISourceEventListener;
|
||||||
import io.github.dsheirer.source.ISourceEventProvider;
|
import io.github.dsheirer.source.ISourceEventProvider;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
|
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||||
|
import java.io.File;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -332,4 +335,38 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
|||||||
{
|
{
|
||||||
return DMRDecoder.this;
|
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.DMRMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
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.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.CapacityMaxAloha;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
||||||
@@ -57,6 +58,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
|
||||||
@@ -65,25 +67,33 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoice
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
|
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.packet.UDTShortMessageService;
|
import io.github.dsheirer.module.decode.dmr.message.data.packet.UDTShortMessageService;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
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.type.ServiceOptions;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
|
||||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||||
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
|
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
|
||||||
|
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
|
||||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
|
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
|
||||||
|
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
|
||||||
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
|
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
|
||||||
|
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
|
||||||
|
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
|
||||||
|
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
|
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
|
||||||
import io.github.dsheirer.util.PacketUtil;
|
import io.github.dsheirer.util.PacketUtil;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
|
||||||
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -102,7 +112,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
private DecodeEvent mCurrentCallEvent;
|
private DecodeEvent mCurrentCallEvent;
|
||||||
private long mCurrentFrequency;
|
private long mCurrentFrequency;
|
||||||
private boolean mIgnoreCRCChecksums;
|
private boolean mIgnoreCRCChecksums;
|
||||||
private Map<Integer,DecodeEvent> mDetectedCallEventsMap = new TreeMap<>();
|
private Map<DMRChannel,DecodeEvent> mDetectedCallEventsMap = new HashMap<>();
|
||||||
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
|
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
|
||||||
new AddChannelRotationActiveStateRequest(State.ACTIVE);
|
new AddChannelRotationActiveStateRequest(State.ACTIVE);
|
||||||
|
|
||||||
@@ -289,7 +299,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp())
|
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp())
|
||||||
.details("MESSAGE: " + sms.getSMS())
|
.details("MESSAGE: " + sms.getSMS())
|
||||||
.identifiers(new IdentifierCollection(sms.getIdentifiers()))
|
.identifiers(new IdentifierCollection(sms.getIdentifiers()))
|
||||||
.timeslot(sms.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
broadcast(smsEvent);
|
broadcast(smsEvent);
|
||||||
}
|
}
|
||||||
@@ -312,11 +322,51 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
|
|
||||||
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp())
|
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp())
|
||||||
.identifiers(mic)
|
.identifiers(mic)
|
||||||
.timeslot(packet.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.details("SMS:" + hyteraSmsPacket.getSMS())
|
.details("SMS:" + hyteraSmsPacket.getSMS())
|
||||||
.build();
|
.build();
|
||||||
broadcast(smsEvent);
|
broadcast(smsEvent);
|
||||||
}
|
}
|
||||||
|
//Hytera Radio Registration Service (RRS)
|
||||||
|
else if(packet.getPacket() instanceof HyteraRrsPacket rrs)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("HYTERA RRS REGISTER RADIO:");
|
||||||
|
sb.append(rrs.getDestination());
|
||||||
|
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||||
|
.identifiers(mic)
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.details(sb.toString())
|
||||||
|
.build();
|
||||||
|
broadcast(shortDataEvent);
|
||||||
|
}
|
||||||
|
//Hytera Short Data
|
||||||
|
else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("HYTERA");
|
||||||
|
|
||||||
|
if(hsdp.getPacketSequence().isEncrypted())
|
||||||
|
{
|
||||||
|
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)hsdp.getPacketSequence().getProprietaryDataHeader();
|
||||||
|
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
|
||||||
|
sb.append(" KEY:").append(hdeh.getKeyId());
|
||||||
|
sb.append(" IV:").append(hdeh.getIV());
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
|
||||||
|
|
||||||
|
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||||
|
.identifiers(mic)
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.details(sb.toString())
|
||||||
|
.build();
|
||||||
|
broadcast(shortDataEvent);
|
||||||
|
}
|
||||||
//Unknown Hytera Long Data Service Token Message
|
//Unknown Hytera Long Data Service Token Message
|
||||||
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
|
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
|
||||||
{
|
{
|
||||||
@@ -324,27 +374,65 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
|
|
||||||
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
|
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
|
||||||
.identifiers(mic)
|
.identifiers(mic)
|
||||||
.timeslot(packet.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
|
.details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
|
||||||
.build();
|
.build();
|
||||||
broadcast(unknownTokenEvent);
|
broadcast(unknownTokenEvent);
|
||||||
}
|
}
|
||||||
|
//Motorola ARS
|
||||||
|
else if(packet.getPacket() instanceof ARSPacket ars)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||||
|
|
||||||
|
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||||
|
.identifiers(mic)
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.details(ars.toString())
|
||||||
|
.build();
|
||||||
|
broadcast(shortDataEvent);
|
||||||
|
}
|
||||||
|
//Motorola LRRP
|
||||||
|
else if(packet.getPacket() instanceof LRRPPacket lrrp)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||||
|
|
||||||
|
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
|
||||||
|
.identifiers(mic)
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.details(lrrp.toString())
|
||||||
|
.build();
|
||||||
|
broadcast(shortDataEvent);
|
||||||
|
}
|
||||||
|
//Motorola XCMP
|
||||||
|
else if(packet.getPacket() instanceof XCMPPacket xcmp)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||||
|
|
||||||
|
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
|
||||||
|
.identifiers(mic)
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.details(xcmp.toString())
|
||||||
|
.build();
|
||||||
|
broadcast(shortDataEvent);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
|
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
|
||||||
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
|
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
|
||||||
.timeslot(packet.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.details(packet.toString())
|
.details(packet.toString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
broadcast(packetEvent);
|
broadcast(packetEvent);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
|
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
|
||||||
|
|
||||||
if (geoPosition != null) {
|
if (geoPosition != null) {
|
||||||
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
|
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
|
||||||
.channel(getCurrentChannel())
|
.channel(getCurrentChannel())
|
||||||
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
|
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
|
||||||
.protocol(Protocol.LRRP)
|
.protocol(Protocol.LRRP)
|
||||||
.location(geoPosition)
|
.location(geoPosition)
|
||||||
.build();
|
.build();
|
||||||
@@ -352,24 +440,43 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
broadcast(plottableDecodeEvent);
|
broadcast(plottableDecodeEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes voice messages
|
* Processes voice messages
|
||||||
*/
|
*/
|
||||||
private void processVoice(VoiceMessage message)
|
private void processVoice(VoiceMessage message)
|
||||||
|
{
|
||||||
|
if(message.getSyncPattern().isMobileSyncPattern())
|
||||||
{
|
{
|
||||||
if(message.getSyncPattern().isDirectMode())
|
if(message.getSyncPattern().isDirectMode())
|
||||||
{
|
{
|
||||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
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()));
|
getIdentifierCollection().update(DMRTalkgroup.create(getTimeslot()));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
|
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(message.getSyncPattern() == DMRSyncPattern.BS_VOICE_FRAME_F && message instanceof VoiceEMBMessage voiceEmb)
|
||||||
|
{
|
||||||
|
if(voiceEmb.hasEmbeddedParameters())
|
||||||
|
{
|
||||||
|
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
|
||||||
|
|
||||||
|
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
|
||||||
|
{
|
||||||
|
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -602,7 +709,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
case HYTERA_08_ANNOUNCEMENT:
|
case HYTERA_08_ANNOUNCEMENT:
|
||||||
case HYTERA_68_ANNOUNCEMENT:
|
case HYTERA_68_ANNOUNCEMENT:
|
||||||
case HYTERA_68_XPT_SITE_STATE:
|
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:
|
case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
|
||||||
if(csbk instanceof CapacityPlusNeighbors)
|
if(csbk instanceof CapacityPlusNeighbors)
|
||||||
{
|
{
|
||||||
@@ -637,21 +759,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
ChannelGrant dataGrant = (ChannelGrant)csbk;
|
ChannelGrant dataGrant = (ChannelGrant)csbk;
|
||||||
DMRChannel channel = dataGrant.getChannel();
|
DMRChannel channel = dataGrant.getChannel();
|
||||||
|
|
||||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
|
||||||
|
|
||||||
if(hasTrafficChannelManager())
|
if(hasTrafficChannelManager())
|
||||||
{
|
{
|
||||||
|
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||||
csbk.getTimestamp(), csbk.isEncrypted());
|
csbk.getTimestamp(), csbk.isEncrypted());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
|
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||||
|
|
||||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||||
{
|
{
|
||||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
|
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||||
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
|
new IdentifierCollection(csbk.getIdentifiers()));
|
||||||
|
mDetectedCallEventsMap.put(channel, event);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -670,21 +793,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
ChannelGrant tgGrant = (ChannelGrant)csbk;
|
ChannelGrant tgGrant = (ChannelGrant)csbk;
|
||||||
DMRChannel channel = tgGrant.getChannel();
|
DMRChannel channel = tgGrant.getChannel();
|
||||||
|
|
||||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
|
||||||
|
|
||||||
if(hasTrafficChannelManager())
|
if(hasTrafficChannelManager())
|
||||||
{
|
{
|
||||||
|
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||||
|
|
||||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
|
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||||
|
|
||||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||||
{
|
{
|
||||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
|
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||||
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
|
new IdentifierCollection(csbk.getIdentifiers()));
|
||||||
|
mDetectedCallEventsMap.put(channel, event);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -702,21 +826,23 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
{
|
{
|
||||||
ChannelGrant channelGrant = (ChannelGrant)csbk;
|
ChannelGrant channelGrant = (ChannelGrant)csbk;
|
||||||
DMRChannel channel = channelGrant.getChannel();
|
DMRChannel channel = channelGrant.getChannel();
|
||||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
|
||||||
|
|
||||||
if(hasTrafficChannelManager())
|
if(hasTrafficChannelManager())
|
||||||
{
|
{
|
||||||
|
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||||
|
|
||||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
|
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||||
|
|
||||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||||
{
|
{
|
||||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel, mergedIdentifiers);
|
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel,
|
||||||
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
|
new IdentifierCollection(csbk.getIdentifiers()));
|
||||||
|
mDetectedCallEventsMap.put(channel, event);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -747,21 +873,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
|
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
|
||||||
DMRChannel channel = cpdcg.getChannel();
|
DMRChannel channel = cpdcg.getChannel();
|
||||||
|
|
||||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
|
||||||
|
|
||||||
if(hasTrafficChannelManager())
|
if(hasTrafficChannelManager())
|
||||||
{
|
{
|
||||||
|
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||||
|
|
||||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||||
csbk.getTimestamp(), csbk.isEncrypted());
|
csbk.getTimestamp(), csbk.isEncrypted());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
|
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||||
|
|
||||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||||
{
|
{
|
||||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
|
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||||
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
|
new IdentifierCollection(csbk.getIdentifiers()));
|
||||||
|
mDetectedCallEventsMap.put(channel, event);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -777,8 +904,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
|
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
|
||||||
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
|
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
|
||||||
.details("Registration Request")
|
.details("Registration Request")
|
||||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||||
.timeslot(csbk.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
broadcast(event);
|
broadcast(event);
|
||||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||||
@@ -786,8 +913,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
|
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
|
||||||
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
|
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
|
||||||
.details("Registration Response")
|
.details("Registration Response")
|
||||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||||
.timeslot(csbk.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
broadcast(regRespEvent);
|
broadcast(regRespEvent);
|
||||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||||
@@ -798,21 +925,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
|
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
|
||||||
DMRChannel channel = cpvcu.getChannel();
|
DMRChannel channel = cpvcu.getChannel();
|
||||||
|
|
||||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
|
||||||
|
|
||||||
if(hasTrafficChannelManager())
|
if(hasTrafficChannelManager())
|
||||||
{
|
{
|
||||||
|
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||||
|
|
||||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||||
csbk.getTimestamp(), csbk.isEncrypted());
|
csbk.getTimestamp(), csbk.isEncrypted());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
|
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel);
|
||||||
|
|
||||||
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
|
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||||
{
|
{
|
||||||
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
|
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||||
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), detectedEvent);
|
new IdentifierCollection(csbk.getIdentifiers()));
|
||||||
|
mDetectedCallEventsMap.put(channel, detectedEvent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -828,8 +956,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
|
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
|
||||||
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
|
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
|
||||||
.details("TALKGROUP AFFILIATION")
|
.details("TALKGROUP AFFILIATION")
|
||||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||||
.timeslot(csbk.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
broadcast(affiliateEvent);
|
broadcast(affiliateEvent);
|
||||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||||
@@ -840,19 +968,20 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel, IdentifierCollection mergedIdentifiers) {
|
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel,
|
||||||
|
IdentifierCollection identifierCollection) {
|
||||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||||
.channel(channel)
|
.channel(channel)
|
||||||
.details(csbk.getOpcode().getLabel())
|
.details(csbk.getOpcode().getLabel())
|
||||||
.identifiers(mergedIdentifiers)
|
.identifiers(identifierCollection)
|
||||||
.timeslot(channel.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
|
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
|
||||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||||
.timeslot(csbk.getTimeslot())
|
.timeslot(getTimeslot())
|
||||||
.details(details)
|
.details(details)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@@ -955,6 +1084,15 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
{
|
{
|
||||||
switch(message.getOpcode())
|
switch(message.getOpcode())
|
||||||
{
|
{
|
||||||
|
case FULL_ENCRYPTION_PARAMETERS:
|
||||||
|
if(message instanceof io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters ep)
|
||||||
|
{
|
||||||
|
if(mCurrentCallEvent != null)
|
||||||
|
{
|
||||||
|
mCurrentCallEvent.setDetails(ep.getDetails());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
|
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
|
||||||
if(message instanceof CapacityPlusRestChannel)
|
if(message instanceof CapacityPlusRestChannel)
|
||||||
{
|
{
|
||||||
@@ -978,11 +1116,9 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
|
case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
|
||||||
if(message instanceof CapacityPlusGroupVoiceChannelUser cpgvcu)
|
if(message instanceof MotorolaGroupVoiceChannelUser cpgvcu)
|
||||||
{
|
{
|
||||||
updateRestChannel(cpgvcu.getRestChannel());
|
|
||||||
|
|
||||||
if(isTerminator)
|
if(isTerminator)
|
||||||
{
|
{
|
||||||
getIdentifierCollection().remove(Role.FROM);
|
getIdentifierCollection().remove(Role.FROM);
|
||||||
@@ -1097,19 +1233,17 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FULL_STANDARD_GPS_INFO:
|
case FULL_STANDARD_GPS_INFO:
|
||||||
if(message instanceof GPSInformation)
|
if(message instanceof GPSInformation gpsInformation)
|
||||||
{
|
{
|
||||||
GPSInformation gpsInformation = (GPSInformation)message;
|
PlottableDecodeEvent plottableGPS = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
|
||||||
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
.channel(getCurrentChannel())
|
||||||
ic.update(gpsInformation.getGPSLocation());
|
|
||||||
|
|
||||||
DecodeEvent gpsEvent = DMRDecodeEvent.builder(DecodeEventType.GPS, message.getTimestamp())
|
|
||||||
.identifiers(ic)
|
|
||||||
.timeslot(message.getTimeslot())
|
|
||||||
.details("LOCATION:" + gpsInformation.getGPSLocation())
|
.details("LOCATION:" + gpsInformation.getGPSLocation())
|
||||||
|
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
|
||||||
|
.protocol(Protocol.DMR)
|
||||||
|
.location(gpsInformation.getPosition())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
broadcast(gpsEvent);
|
broadcast(plottableGPS);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
|
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
|
||||||
@@ -1121,6 +1255,41 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current call with encryption information.
|
||||||
|
* @param encryptionParameters decoded from the Voice Frame F
|
||||||
|
* @param isGroup true for group or false for individual call.
|
||||||
|
*/
|
||||||
|
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
|
||||||
|
{
|
||||||
|
if(mCurrentCallEvent != null)
|
||||||
|
{
|
||||||
|
String details = mCurrentCallEvent.getDetails();;
|
||||||
|
|
||||||
|
if(details == null)
|
||||||
|
{
|
||||||
|
details = encryptionParameters.toString();
|
||||||
|
}
|
||||||
|
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
|
||||||
|
{
|
||||||
|
details += " " + encryptionParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentCallEvent.setDetails(details);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mCurrentCallEvent = DMRDecodeEvent.builder(isGroup ? DecodeEventType.CALL_GROUP_ENCRYPTED :
|
||||||
|
DecodeEventType.CALL_ENCRYPTED, timestamp)
|
||||||
|
.channel(getCurrentChannel())
|
||||||
|
.details(encryptionParameters.toString())
|
||||||
|
.identifiers(getIdentifierCollection().copyOf())
|
||||||
|
.timeslot(getTimeslot())
|
||||||
|
.build();
|
||||||
|
broadcast(mCurrentCallEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates or creates a current call event.
|
* Updates or creates a current call event.
|
||||||
*
|
*
|
||||||
@@ -1138,13 +1307,14 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
.channel(getCurrentChannel())
|
.channel(getCurrentChannel())
|
||||||
.details(details)
|
.details(details)
|
||||||
.identifiers(getIdentifierCollection().copyOf())
|
.identifiers(getIdentifierCollection().copyOf())
|
||||||
|
.timeslot(getTimeslot())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
broadcast(mCurrentCallEvent);
|
broadcast(mCurrentCallEvent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if(type != DecodeEventType.CALL)
|
if(mCurrentCallEvent.getDetails() == null)
|
||||||
{
|
{
|
||||||
mCurrentCallEvent.setDetails(details);
|
mCurrentCallEvent.setDetails(details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.DMRBurst;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
|
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.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.Aloha;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
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;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
|
||||||
@@ -44,6 +45,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
|
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceSuperFrameProcessor;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -59,6 +62,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
|
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
|
||||||
private DecodeConfigDMR mConfigDMR;
|
private DecodeConfigDMR mConfigDMR;
|
||||||
|
private VoiceSuperFrameProcessor mSuperFrameProcessor1 = new VoiceSuperFrameProcessor();
|
||||||
|
private VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor();
|
||||||
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
|
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
|
||||||
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
|
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
|
||||||
private MBCAssembler mMBCAssembler = new MBCAssembler();
|
private MBCAssembler mMBCAssembler = new MBCAssembler();
|
||||||
@@ -67,6 +72,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
||||||
private Listener<IMessage> mMessageListener;
|
private Listener<IMessage> mMessageListener;
|
||||||
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
||||||
|
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
@@ -89,19 +95,64 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
@Override
|
@Override
|
||||||
public void receive(IMessage message)
|
public void receive(IMessage message)
|
||||||
{
|
{
|
||||||
//Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings
|
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)
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor1.process(flc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor2.process(flc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(message instanceof VoiceMessage voiceMessage)
|
||||||
|
{
|
||||||
|
if(voiceMessage.getTimeslot() == 1)
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor1.process(voiceMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor2.process(voiceMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(message instanceof DMRBurst dmrBurst && dmrBurst.isValid())
|
||||||
|
{
|
||||||
|
if(dmrBurst.getTimeslot() == 1)
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor1.reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mSuperFrameProcessor2.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
|
||||||
if(message instanceof ITimeslotFrequencyReceiver)
|
if(message instanceof ITimeslotFrequencyReceiver)
|
||||||
{
|
{
|
||||||
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
|
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
|
||||||
int[] lsns = receiver.getLogicalTimeslotNumbers();
|
int[] lcns = receiver.getLogicalChannelNumbers();
|
||||||
|
|
||||||
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
|
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
|
||||||
|
|
||||||
for(int lsn: lsns)
|
for(int lcn: lcns)
|
||||||
{
|
{
|
||||||
if(mTimeslotFrequencyMap.containsKey(lsn))
|
if(mTimeslotFrequencyMap.containsKey(lcn))
|
||||||
{
|
{
|
||||||
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn));
|
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +166,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
dispatch(message);
|
dispatch(message);
|
||||||
|
|
||||||
//Extract the Full Link Control message fragment from the Voice with embedded signalling 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)
|
if(message.getTimeslot() == 1)
|
||||||
{
|
{
|
||||||
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
package io.github.dsheirer.module.decode.dmr;
|
||||||
@@ -41,14 +38,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusCo
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.type.Model;
|
import io.github.dsheirer.module.decode.dmr.message.type.Model;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the network configuration details of a DMR network from the broadcast messages
|
* Tracks the network configuration details of a DMR network from the broadcast messages
|
||||||
@@ -250,7 +246,7 @@ public class DMRNetworkConfigurationMonitor
|
|||||||
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
|
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
|
||||||
mNeighborSites.addAll(cpnr.getNeighbors());
|
mNeighborSites.addAll(cpnr.getNeighbors());
|
||||||
}
|
}
|
||||||
if(mBrand == null)
|
if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
|
||||||
{
|
{
|
||||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||||
}
|
}
|
||||||
@@ -279,7 +275,7 @@ public class DMRNetworkConfigurationMonitor
|
|||||||
*/
|
*/
|
||||||
private void addDmrChannel(DMRChannel dmrChannel)
|
private void addDmrChannel(DMRChannel dmrChannel)
|
||||||
{
|
{
|
||||||
mObservedChannelMap.put(dmrChannel.getLogicalSlotNumber(), dmrChannel);
|
mObservedChannelMap.put(dmrChannel.getValue(), dmrChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset()
|
public void reset()
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -91,9 +91,17 @@ public enum DMRSyncPattern
|
|||||||
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
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);
|
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
||||||
|
|
||||||
//Direct Mode Sync Patterns
|
//Direct Mode TS 1 Sync Patterns
|
||||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
|
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
|
||||||
DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
|
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
|
* Pattern that represents the enum entry
|
||||||
@@ -140,7 +148,43 @@ public enum DMRSyncPattern
|
|||||||
*/
|
*/
|
||||||
public boolean isDirectMode()
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
private List<Channel> mManagedTrafficChannels;
|
private List<Channel> mManagedTrafficChannels;
|
||||||
|
|
||||||
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
|
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
|
||||||
private Map<Integer,DMRChannelGrantEvent> mLSNGrantEventMap = new ConcurrentHashMap<>();
|
private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private Listener<ChannelEvent> mChannelEventListener;
|
private Listener<ChannelEvent> mChannelEventListener;
|
||||||
private Listener<IDecodeEvent> mDecodeEventListener;
|
private Listener<IDecodeEvent> mDecodeEventListener;
|
||||||
@@ -322,9 +322,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
|
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
|
||||||
Opcode opcode, long timestamp, boolean encrypted)
|
Opcode opcode, long timestamp, boolean encrypted)
|
||||||
{
|
{
|
||||||
int lsn = channel.getLogicalSlotNumber();
|
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||||
|
|
||||||
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
|
|
||||||
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
||||||
|
|
||||||
if(isStale(event, timestamp, identifierCollection)) //Create new event
|
if(isStale(event, timestamp, identifierCollection)) //Create new event
|
||||||
@@ -335,7 +333,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
.identifiers(identifierCollection)
|
.identifiers(identifierCollection)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mLSNGrantEventMap.put(lsn, event);
|
mChannelGrantEventMap.put(channel, event);
|
||||||
|
|
||||||
}
|
}
|
||||||
else //Update current event
|
else //Update current event
|
||||||
@@ -355,7 +353,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
.identifiers(identifierCollection)
|
.identifiers(identifierCollection)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mLSNGrantEventMap.put(lsn, event);
|
mChannelGrantEventMap.put(channel, event);
|
||||||
broadcast(event);
|
broadcast(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,37 +641,37 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
*/
|
*/
|
||||||
private void removeCallEvents(long frequency)
|
private void removeCallEvents(long frequency)
|
||||||
{
|
{
|
||||||
List<Integer> lsnsToRemove = new ArrayList<>();
|
List<DMRChannel> channelsToRemove = new ArrayList<>();
|
||||||
|
|
||||||
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
|
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||||
{
|
{
|
||||||
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
if(entry.getKey().getDownlinkFrequency() == frequency)
|
||||||
{
|
{
|
||||||
lsnsToRemove.add(entry.getKey());
|
channelsToRemove.add(entry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Integer lsn: lsnsToRemove)
|
for(DMRChannel channel: channelsToRemove)
|
||||||
{
|
{
|
||||||
mLSNGrantEventMap.remove(lsn);
|
mChannelGrantEventMap.remove(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCallEventDetails(long frequency, String detailFragment)
|
private void updateCallEventDetails(long frequency, String detailFragment)
|
||||||
{
|
{
|
||||||
List<Integer> lsnsToUpdate = new ArrayList<>();
|
List<DMRChannel> channelsToUpdate = new ArrayList<>();
|
||||||
|
|
||||||
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
|
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||||
{
|
{
|
||||||
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
||||||
{
|
{
|
||||||
lsnsToUpdate.add(entry.getKey());
|
channelsToUpdate.add(entry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Integer lsn: lsnsToUpdate)
|
for(DMRChannel channel: channelsToUpdate)
|
||||||
{
|
{
|
||||||
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
|
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||||
|
|
||||||
if(event != null)
|
if(event != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -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.AbstractVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
|
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.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.VoiceEMBMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
@@ -104,44 +103,24 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
|||||||
public void receive(IMessage message)
|
public void receive(IMessage message)
|
||||||
{
|
{
|
||||||
if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
|
if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
|
||||||
|
{
|
||||||
|
//Attempt to set the audio encryption state from certain types of messages
|
||||||
|
if(!mEncryptedCallStateEstablished)
|
||||||
{
|
{
|
||||||
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
|
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
|
||||||
if(!mEncryptedCallStateEstablished && message instanceof VoiceMessage vm &&
|
if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
|
||||||
vm.getSyncPattern().isDirectMode())
|
|
||||||
{
|
{
|
||||||
mEncryptedCallStateEstablished = true;
|
mEncryptedCallStateEstablished = true;
|
||||||
mEncryptedCall = false;
|
mEncryptedCall = false;
|
||||||
List<byte[]> frames = vm.getAMBEFrames();
|
|
||||||
for(byte[] frame: frames)
|
|
||||||
{
|
|
||||||
processAudio(frame, message.getTimestamp());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
|
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
|
||||||
//in the EMB field.
|
//in the EMB field.
|
||||||
else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage voiceMessage)
|
else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
|
||||||
{
|
|
||||||
if(voiceMessage.getEMB().isValid())
|
|
||||||
{
|
{
|
||||||
mEncryptedCallStateEstablished = true;
|
mEncryptedCallStateEstablished = true;
|
||||||
mEncryptedCall = voiceMessage.getEMB().isEncrypted();
|
mEncryptedCall = voiceMessage.getEMB().isEncrypted();
|
||||||
}
|
}
|
||||||
|
else if(message instanceof VoiceHeader voiceHeader)
|
||||||
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();
|
LCMessage lc = voiceHeader.getLCMessage();
|
||||||
|
|
||||||
@@ -154,14 +133,25 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Note: the DMRMessageProcess extracts Full Link Control messages from Voice Frames B-C and sends them
|
//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
|
// 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.
|
// 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)
|
else if(message instanceof AbstractVoiceChannelUser avcu)
|
||||||
{
|
{
|
||||||
ServiceOptions serviceOptions = avcu.getServiceOptions();
|
|
||||||
mEncryptedCallStateEstablished = true;
|
mEncryptedCallStateEstablished = true;
|
||||||
mEncryptedCall = serviceOptions.isEncrypted();
|
mEncryptedCall = avcu.getServiceOptions().isEncrypted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//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)
|
||||||
|
{
|
||||||
|
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||||
|
for(byte[] frame: frames)
|
||||||
|
{
|
||||||
|
processAudio(frame, message.getTimestamp());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(message instanceof Terminator)
|
else if(message instanceof Terminator)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -32,17 +32,16 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChann
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
|
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
|
import java.util.List;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DMR AMBE Frame recorder generates call sequence recordings containing JSON representations of audio
|
* DMR AMBE Frame recorder generates call sequence recordings containing JSON representations of audio
|
||||||
* frames, optional encryption and call identifiers.
|
* frames, optional encryption and call identifiers.
|
||||||
@@ -206,8 +205,8 @@ public class DMRCallSequenceRecorder extends MBECallSequenceRecorder
|
|||||||
{
|
{
|
||||||
switch(message.getOpcode())
|
switch(message.getOpcode())
|
||||||
{
|
{
|
||||||
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
|
case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
|
||||||
if(message instanceof CapacityPlusGroupVoiceChannelUser cpvcu)
|
if(message instanceof MotorolaGroupVoiceChannelUser cpvcu)
|
||||||
{
|
{
|
||||||
if(mCallSequence == null)
|
if(mCallSequence == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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.channel;
|
package io.github.dsheirer.module.decode.dmr.channel;
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -26,6 +26,8 @@ import io.github.dsheirer.identifier.Role;
|
|||||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +35,9 @@ import org.apache.commons.lang3.Validate;
|
|||||||
*
|
*
|
||||||
* Note: timeslots are tracked as 1 and 2
|
* Note: timeslots are tracked as 1 and 2
|
||||||
*/
|
*/
|
||||||
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor
|
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
|
||||||
{
|
{
|
||||||
|
private TimeslotFrequency mTimeslotFrequency;
|
||||||
private int mTimeslot;
|
private int mTimeslot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,13 +59,23 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeater number for the channel.
|
* Repeater number aka Logical Channel Number (LCN) for the channel.
|
||||||
*/
|
*/
|
||||||
public int getRepeater()
|
public int getChannelNumber()
|
||||||
{
|
{
|
||||||
return getValue();
|
return getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to format the single channel number as an array for TimeslotFrequency matching.
|
||||||
|
* @return channel number as an integer array.
|
||||||
|
*/
|
||||||
|
public int[] getLogicalChannelNumbers()
|
||||||
|
{
|
||||||
|
return new int[]{getChannelNumber()};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timeslot for the channel.
|
* Timeslot for the channel.
|
||||||
* @return timeslot as zero-based index with values in range: 0 or 1.
|
* @return timeslot as zero-based index with values in range: 0 or 1.
|
||||||
@@ -72,36 +85,6 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
|||||||
return mTimeslot;
|
return mTimeslot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Logical slot number for this channel. LSN is a 1-based index value where repeater one, timeslot 1 is
|
|
||||||
* LSN 1, timeslot 2 is LSN 2, etc.
|
|
||||||
*
|
|
||||||
* Formula: LSN = ((channel - 1) * 2) + timeslot
|
|
||||||
*
|
|
||||||
* @return logical slot number, a 1-based index value
|
|
||||||
*/
|
|
||||||
public int getLogicalSlotNumber()
|
|
||||||
{
|
|
||||||
int repeater = getRepeater();
|
|
||||||
|
|
||||||
if(repeater > 0)
|
|
||||||
{
|
|
||||||
return ((repeater - 1) * 2) + getTimeslot();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of length 1 containing this channel's logical slot number
|
|
||||||
*/
|
|
||||||
public int[] getLSNArray()
|
|
||||||
{
|
|
||||||
int[] logicalSlotNumbers = new int[1];
|
|
||||||
logicalSlotNumbers[0] = getLogicalSlotNumber();
|
|
||||||
return logicalSlotNumbers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of timeslots for the DMR channel.
|
* Number of timeslots for the DMR channel.
|
||||||
* @return 2 always.
|
* @return 2 always.
|
||||||
@@ -121,6 +104,13 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "CHAN:" + getChannelNumber() + ":" + getTimeslot();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Not implemented
|
* Not implemented
|
||||||
*/
|
*/
|
||||||
@@ -138,4 +128,72 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
|||||||
{
|
{
|
||||||
throw new IllegalArgumentException("This method is not supported");
|
throw new IllegalArgumentException("This method is not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDownlinkFrequency()
|
||||||
|
{
|
||||||
|
if(mTimeslotFrequency != null)
|
||||||
|
{
|
||||||
|
return mTimeslotFrequency.getDownlinkFrequency();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUplinkFrequency()
|
||||||
|
{
|
||||||
|
if(mTimeslotFrequency != null)
|
||||||
|
{
|
||||||
|
return mTimeslotFrequency.getUplinkFrequency();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the lsn to frequency mapper value.
|
||||||
|
* @param timeslotFrequency to set
|
||||||
|
*/
|
||||||
|
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
|
||||||
|
{
|
||||||
|
mTimeslotFrequency = timeslotFrequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||||
|
{
|
||||||
|
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||||
|
{
|
||||||
|
if(timeslotFrequency.getChannelNumber() == getChannelNumber())
|
||||||
|
{
|
||||||
|
setTimeslotFrequency(timeslotFrequency);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if(this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(!(o instanceof DMRChannel that))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!super.equals(o))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getTimeslot() == that.getTimeslot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(super.hashCode(), getTimeslot());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* *****************************************************************************
|
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.dmr.channel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DMR logical channel. This channel uses a logical channel number and a timeslot.
|
|
||||||
*/
|
|
||||||
public class DMRLogicalChannel extends DMRChannel
|
|
||||||
{
|
|
||||||
private TimeslotFrequency mTimeslotFrequency;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
|
|
||||||
* calculated logical slot value for visual compatibility for users.
|
|
||||||
*
|
|
||||||
* @param channel number or repeater number
|
|
||||||
* @param logicalSlotNumber - zero based index.
|
|
||||||
*/
|
|
||||||
public DMRLogicalChannel(int channel, int timeslot)
|
|
||||||
{
|
|
||||||
super(channel, timeslot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downlink frequency
|
|
||||||
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long getDownlinkFrequency()
|
|
||||||
{
|
|
||||||
if(mTimeslotFrequency != null)
|
|
||||||
{
|
|
||||||
return mTimeslotFrequency.getDownlinkFrequency();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uplink frequency
|
|
||||||
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long getUplinkFrequency()
|
|
||||||
{
|
|
||||||
if(mTimeslotFrequency != null)
|
|
||||||
{
|
|
||||||
return mTimeslotFrequency.getUplinkFrequency();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the timeslot frequency mapping
|
|
||||||
* @param timeslotFrequency
|
|
||||||
*/
|
|
||||||
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
|
|
||||||
{
|
|
||||||
mTimeslotFrequency = timeslotFrequency;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formatted channel number
|
|
||||||
*/
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("LSN:").append(getLogicalSlotNumber());
|
|
||||||
// sb.append(" LCN:").append(getValue());
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMR Logical Slot Number (LSN) channel.
|
||||||
|
*
|
||||||
|
* Note: LSN 1-16 are represented in memory using the following channel and timeslot values:
|
||||||
|
*
|
||||||
|
* LSN: CHANNEL/TIMESLOT
|
||||||
|
* 1: 1/1
|
||||||
|
* 2: 1/2
|
||||||
|
* 3: 2/1
|
||||||
|
* 4: 2/2
|
||||||
|
* 5: 3/1
|
||||||
|
* 6: 3/2
|
||||||
|
* 7: 4/1
|
||||||
|
* 8: 4/2
|
||||||
|
* 9: 5/1
|
||||||
|
* 10: 5/2
|
||||||
|
* 11: 6/1
|
||||||
|
* 12: 6/2
|
||||||
|
* 13: 7/1
|
||||||
|
* 14: 7/2
|
||||||
|
* 15: 8/1
|
||||||
|
* 16: 8/2
|
||||||
|
*/
|
||||||
|
public class DMRLsn extends DMRChannel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*
|
||||||
|
* @param lsn in range 1 - 16
|
||||||
|
*/
|
||||||
|
public DMRLsn(int lsn)
|
||||||
|
{
|
||||||
|
super(((lsn - 1) / 2) + 1, ((lsn - 1) % 2) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "LSN:" + getLsn() + " LCN:" + getChannelNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical Slot Number
|
||||||
|
* @return lsn
|
||||||
|
*/
|
||||||
|
public int getLsn()
|
||||||
|
{
|
||||||
|
return ((getChannelNumber() - 1) * 2) + getTimeslot();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,16 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.channel;
|
package io.github.dsheirer.module.decode.dmr.channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DMR Tier III Trunking Channel
|
* DMR Tier III Trunking channel. This channel uses a logical channel number and a timeslot.
|
||||||
*/
|
*/
|
||||||
public class DMRTier3Channel extends DMRLogicalChannel
|
public class DMRTier3Channel extends DMRChannel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
|
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
|
||||||
* calculated logical slot value for visual compatibility for users.
|
* calculated logical slot value for visual compatibility for users.
|
||||||
*
|
*
|
||||||
* @param channel number or repeater number
|
* @param channel number or repeater number
|
||||||
* @param timeslot
|
* @param timeslot either 1 or 2.
|
||||||
*/
|
*/
|
||||||
public DMRTier3Channel(int channel, int timeslot)
|
public DMRTier3Channel(int channel, int timeslot)
|
||||||
{
|
{
|
||||||
@@ -18,23 +37,22 @@ public class DMRTier3Channel extends DMRLogicalChannel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logical slot number for this channel.
|
* Tier III Channel ID
|
||||||
*
|
* @return channel ID
|
||||||
* Formula: LSN = (channel * 2) + timeslot
|
|
||||||
*
|
|
||||||
* @return logical slot number, a 1-based index value
|
|
||||||
*/
|
*/
|
||||||
@Override
|
public int getChannelId()
|
||||||
public int getLogicalSlotNumber()
|
|
||||||
{
|
{
|
||||||
int repeater = getRepeater();
|
return getChannelNumber() * 2 + getTimeslot();
|
||||||
|
}
|
||||||
|
|
||||||
if(repeater > 0)
|
/**
|
||||||
|
* Formatted channel number
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
{
|
{
|
||||||
return ((repeater) * 2) + getTimeslot();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(" LCN:").append(getChannelNumber());
|
||||||
|
sb.append(" CHANID:").append(getChannelId());
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMR Rest Channel logical slot number (LSN).
|
||||||
|
*/
|
||||||
|
public class DmrRestLsn extends DMRLsn
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*
|
||||||
|
* @param lsn in range 1 - 16
|
||||||
|
*/
|
||||||
|
public DmrRestLsn(int lsn)
|
||||||
|
{
|
||||||
|
super(lsn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "REST:" + getLsn();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -24,9 +24,9 @@ import java.util.List;
|
|||||||
public interface ITimeslotFrequencyReceiver
|
public interface ITimeslotFrequencyReceiver
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Provides the logical slot number(s) that require a matching timeslot frequency mapping
|
* Provides the logical channel number(s) that require a matching timeslot frequency mapping
|
||||||
*/
|
*/
|
||||||
public int[] getLogicalTimeslotNumbers();
|
public int[] getLogicalChannelNumbers();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the list of timeslot frequency mappings to the implementer
|
* Applies the list of timeslot frequency mappings to the implementer
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -31,7 +31,7 @@ import javafx.beans.property.SimpleLongProperty;
|
|||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a timeslot number to a pair of channel frequency values
|
* Maps a logical channel number (LCN) to a pair of channel frequency values
|
||||||
*/
|
*/
|
||||||
public class TimeslotFrequency
|
public class TimeslotFrequency
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ public class TimeslotFrequency
|
|||||||
* Downlink Frequency property
|
* Downlink Frequency property
|
||||||
*/
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public LongProperty downlinkFrequencyPropertyProperty()
|
public LongProperty downlinkFrequencyProperty()
|
||||||
{
|
{
|
||||||
return mDownlinkFrequencyProperty;
|
return mDownlinkFrequencyProperty;
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,10 @@ public class TimeslotFrequency
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logical slot number (LSN) as a 1-index based counter
|
* Logical channel number (LCN) as a 1-index based counter
|
||||||
|
*
|
||||||
|
* Note: lsn (logical slot number) is a legacy name for this field. The field currently holds the LCN or logical
|
||||||
|
* channel number. To avoid breaking serialization, we leave this field labeled as lsn.
|
||||||
*/
|
*/
|
||||||
@JacksonXmlProperty(isAttribute = true, localName = "lsn")
|
@JacksonXmlProperty(isAttribute = true, localName = "lsn")
|
||||||
public int getNumber()
|
public int getNumber()
|
||||||
@@ -109,12 +112,52 @@ public class TimeslotFrequency
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the logical slot number (LSN) as a 1-index start
|
* Logical channel number which is the same value as getNumber.
|
||||||
* @param lsn where LSN 1 is the first slot, 2 the second, etc
|
* @return
|
||||||
*/
|
*/
|
||||||
public void setNumber(int lsn)
|
@JsonIgnore
|
||||||
|
public int getChannelNumber()
|
||||||
{
|
{
|
||||||
mNumberProperty.set(lsn);
|
return getNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public String getLogicalSlotNumbers()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
int channel = getChannelNumber();
|
||||||
|
|
||||||
|
if(channel > 0)
|
||||||
|
{
|
||||||
|
if(channel < 32) //Connect Plus uses 5-bits for LSN for a maximum value of 31
|
||||||
|
{
|
||||||
|
//Cap+ & Con+ Logical Slot Numbers
|
||||||
|
int lsn1 = (channel - 1) * 2 + 1;
|
||||||
|
int lsn2 = (channel - 1) * 2 + 2;
|
||||||
|
sb.append("LSN:").append(lsn1).append("/").append(lsn2).append(" or ");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Tier III channel IDs
|
||||||
|
int chanId1 = channel * 2 + 1;
|
||||||
|
int chanId2 = channel * 2 + 2;
|
||||||
|
sb.append("Tier3 Chan:").append(chanId1).append("/").append(chanId2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("(empty)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the logical channel number (LCN) as a 1-index start
|
||||||
|
* @param lcn where LCN 1 is logical slot numbers (LSN) 1 and 2, LCN2=LSN3/4, etc.
|
||||||
|
*/
|
||||||
|
public void setNumber(int lcn)
|
||||||
|
{
|
||||||
|
mNumberProperty.set(lcn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,7 +203,7 @@ public class TimeslotFrequency
|
|||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return "TIMESLOT LSN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
|
return "LCN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +211,7 @@ public class TimeslotFrequency
|
|||||||
*/
|
*/
|
||||||
public static Callback<TimeslotFrequency,Observable[]> extractor()
|
public static Callback<TimeslotFrequency,Observable[]> extractor()
|
||||||
{
|
{
|
||||||
return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyPropertyProperty(),
|
return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyProperty(),
|
||||||
tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()};
|
tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user