mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 15:47:25 -05:00
Compare commits
88 Commits
v0.6.0
...
v0.6.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4977c3e4fa | ||
|
|
54a0ca5247 | ||
|
|
024a624083 | ||
|
|
9a02416403 | ||
|
|
91e5bc8ba1 | ||
|
|
97afdfa8a9 | ||
|
|
67f91b80f2 | ||
|
|
1b3ce431e6 | ||
|
|
d24895575c | ||
|
|
c97e355349 | ||
|
|
3c8cd0386d | ||
|
|
600db3fb8a | ||
|
|
161595e7c8 | ||
|
|
0ecdf4dd2b | ||
|
|
32ce79b8af | ||
|
|
8070ae3c4b | ||
|
|
55b20c75c0 | ||
|
|
5d4dff7948 | ||
|
|
b1229b6971 | ||
|
|
9474d53a93 | ||
|
|
6f05db723f | ||
|
|
056bccb89a | ||
|
|
e00cd90aef | ||
|
|
8bb59d581f | ||
|
|
b9068da99c | ||
|
|
9f3f7e65a4 | ||
|
|
e6a804836b | ||
|
|
5d8d3e4a41 | ||
|
|
298d9d7016 | ||
|
|
d7fbbcefc5 | ||
|
|
36e198eb1b | ||
|
|
130e59f4c1 | ||
|
|
9223e7c056 | ||
|
|
c3487411fb | ||
|
|
142dd9d579 | ||
|
|
53d537bda8 | ||
|
|
24e11b8ed6 | ||
|
|
14a7b040f9 | ||
|
|
2af3e992a1 | ||
|
|
737c3faacc | ||
|
|
03792b0245 | ||
|
|
dddda052fa | ||
|
|
c50a914632 | ||
|
|
5f082973eb | ||
|
|
2f0419f796 | ||
|
|
cbe451a793 | ||
|
|
aa8d449140 | ||
|
|
7e709d9d6f | ||
|
|
f5938fa8e2 | ||
|
|
ca2509e9f9 | ||
|
|
1d2ba8c06c | ||
|
|
73275aa8ae | ||
|
|
2c32096ec9 | ||
|
|
6532e33296 | ||
|
|
7ea8cb63d2 | ||
|
|
1df29b62eb | ||
|
|
8a5b31f143 | ||
|
|
4d7a8e5980 | ||
|
|
aeeed71231 | ||
|
|
f6c4cc8a5f | ||
|
|
d019388034 | ||
|
|
e125d6a452 | ||
|
|
6b5a1fb827 | ||
|
|
0491b08818 | ||
|
|
7b9adc1da2 | ||
|
|
3acaea0019 | ||
|
|
be02393f65 | ||
|
|
614bf72f01 | ||
|
|
bed28b82ab | ||
|
|
c047d48d91 | ||
|
|
5222bffce4 | ||
|
|
42e7a49d37 | ||
|
|
686bddf7d5 | ||
|
|
1c420b8eee | ||
|
|
e2a0325d07 | ||
|
|
9d03c151af | ||
|
|
3c18bfdc81 | ||
|
|
d89ba65238 | ||
|
|
d2cf2ee7b5 | ||
|
|
7a03331955 | ||
|
|
7c5ef3803a | ||
|
|
6f3e7c86e5 | ||
|
|
fc51de2b4f | ||
|
|
e1e75710cb | ||
|
|
87ce08e9b9 | ||
|
|
1a5c7521a5 | ||
|
|
b54e94dfbe | ||
|
|
19977d5f4d |
4
.github/workflows/gradle.yml
vendored
4
.github/workflows/gradle.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 20
|
||||
- name: Set up JDK 23
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '20'
|
||||
java-version: '23'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 20
|
||||
- name: Set up JDK 23
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '20'
|
||||
java-version: '23'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
|
||||
|
||||
1
.idea/dictionaries/denny.xml
generated
1
.idea/dictionaries/denny.xml
generated
@@ -29,6 +29,7 @@
|
||||
<w>talkgroups</w>
|
||||
<w>tgid</w>
|
||||
<w>ultravox</w>
|
||||
<w>unsynchronized</w>
|
||||
<w>wacn</w>
|
||||
<w>xtea</w>
|
||||
</words>
|
||||
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23_PREVIEW" project-jdk-name="liberica-23" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,36 +0,0 @@
|
||||
Auto-generating Java code from SDRPlay API using jextract.
|
||||
|
||||
Note: the api headers in the 3.11 folder are equivalent to the 3.08 headers. The only
|
||||
difference from the 3.07 headers is the addition of the boolean 'valid' flag to DeviceT structure.
|
||||
|
||||
1. Download JDK19 compatible build of jextract JDK: https://jdk.java.net/jextract/
|
||||
2. If generating on windows platform, modify the sdrplay_api.h to remove the windows
|
||||
pieces of the file. Specifically:
|
||||
2a. Comment out these lines but leave the *HANDLE typedef:
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
//#endif
|
||||
//#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
2b. Remove the '_SDRPLAY_DLL_QUALIFIER' from in front of the method declarations
|
||||
|
||||
2c. Change the 3.11 version string to 3.08 either in the header file, or in the generated java class
|
||||
|
||||
3. Open command prompt to the root source code where the generated files should be placed:
|
||||
..\SDRtrunk\sdrplay-api\src\main\java
|
||||
|
||||
4. Generate the code:
|
||||
c:\Users\Denny\Downloads\jdk-19-ea2-panama\bin\jextract "C:\Users\Denny\git\sdrtrunk\sdrplay-api\api\v3_11\sdrplay_api.h" -l libsdrplay_api --source
|
||||
|
||||
5. Do this for all versions of the API.
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
#include "sdrplay_api_rx_channel.h"
|
||||
#include "sdrplay_api_callback.h"
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
#include "windows.h"
|
||||
#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
#endif
|
||||
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
//#endif
|
||||
//#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
#define _SDRPLAY_DLL_QUALIFIER
|
||||
#endif
|
||||
#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
// Application code should check that it is compiled against the same API version
|
||||
// sdrplay_api_ApiVersion() returns the API version
|
||||
#define SDRPLAY_API_VERSION (float)(3.11)
|
||||
#define SDRPLAY_API_VERSION (float)(3.15)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
@@ -35,6 +35,8 @@ typedef void *HANDLE;
|
||||
#define SDRPLAY_RSP2_ID (2)
|
||||
#define SDRPLAY_RSPduo_ID (3)
|
||||
#define SDRPLAY_RSPdx_ID (4)
|
||||
#define SDRPLAY_RSP1B_ID (6)
|
||||
#define SDRPLAY_RSPdxR2_ID (7)
|
||||
|
||||
// Enum types
|
||||
typedef enum
|
||||
@@ -125,6 +127,7 @@ typedef enum
|
||||
sdrplay_api_Update_RspDx_RfNotchControl = 0x00000008,
|
||||
sdrplay_api_Update_RspDx_RfDabNotchControl = 0x00000010,
|
||||
sdrplay_api_Update_RspDx_HdrBw = 0x00000020,
|
||||
sdrplay_api_Update_RspDuo_ResetSlaveFlags = 0x00000040,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
} sdrplay_api_ReasonForUpdateExtension1T;
|
||||
@@ -178,6 +181,7 @@ typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_Device
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ReleaseDevice_t)(sdrplay_api_DeviceT *device);
|
||||
typedef const char* (*sdrplay_api_GetErrorString_t)(sdrplay_api_ErrT err);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastError_t)(sdrplay_api_DeviceT *device);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastErrorByType_t)(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DisableHeartbeat_t)(void);
|
||||
|
||||
// Device API function types
|
||||
@@ -199,27 +203,28 @@ extern "C"
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastErrorByType(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
|
||||
// Device API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
@@ -29,12 +29,19 @@ typedef struct
|
||||
} sdrplay_api_RspDuoParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
} sdrplay_api_RspDuo_ResetSlaveFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel; // default: sdrplay_api_RspDuo_AMPORT_2
|
||||
unsigned char tuner1AmNotchEnable; // default: 0
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
sdrplay_api_RspDuo_ResetSlaveFlagsT resetSlaveFlags;
|
||||
} sdrplay_api_RspDuoTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPduo_H
|
||||
64
build.gradle
64
build.gradle
@@ -1,8 +1,6 @@
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,12 +17,16 @@ import java.text.SimpleDateFormat
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instructions for building/compiling the sdrtrunk application.
|
||||
*
|
||||
* Prerequisites:
|
||||
* Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 7.6+
|
||||
* Install and configure an OpenJDK version 22+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 8.10+
|
||||
*
|
||||
* Scenario 1: run the application via gradle command line from the source code root directory:
|
||||
* command: ./gradlew run
|
||||
@@ -56,10 +58,10 @@ repositories {
|
||||
//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 23 is required for this version of the Project Panama preview/incubator feature
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(20)
|
||||
languageVersion = JavaLanguageVersion.of(23)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +77,11 @@ test {
|
||||
dependencies {
|
||||
|
||||
// JUnit Tests
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.assertj:assertj-core:3.23.1'
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.9.0')
|
||||
testImplementation 'org.mockito:mockito-core:4.8.0'
|
||||
testImplementation(platform("org.junit:junit-bom:5.11.0"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
|
||||
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
@@ -88,8 +89,8 @@ dependencies {
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.12'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
@@ -97,16 +98,16 @@ dependencies {
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'com.google.guava:guava:33.3.0-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:11.0'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
implementation 'org.apache.commons:commons-compress:1.21'
|
||||
implementation 'org.apache.commons:commons-compress:1.26.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
@@ -125,11 +126,11 @@ dependencies {
|
||||
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
|
||||
|
||||
/**
|
||||
* This is needed for the JDK19 panama and vector API ... until it moves out of incubation or preview
|
||||
* This is needed for the JDK23 vector API ... until it moves out of incubation or preview
|
||||
*/
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs.add("--enable-preview") //Panama Foreign Function/Memory is preview in JDK 19
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Needed while Panama Vector API remains in incubator
|
||||
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,10 +139,10 @@ tasks.withType(JavaCompile) {
|
||||
def jvmArgsWindows =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //Allows Project Panama Foreign Function access to system library
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector API is still in incubator
|
||||
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
|
||||
|
||||
/**
|
||||
* JVM Arguments for running the application on Linux.
|
||||
@@ -150,8 +151,7 @@ def jvmArgsLinux =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=/usr/local/lib'] //Allows Project Panama Foreign Function access to system library
|
||||
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
|
||||
|
||||
application {
|
||||
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
|
||||
@@ -198,16 +198,16 @@ String targetWindowsX86_64 = 'windows-x86_64-v' + version
|
||||
* author's development environment, but can be changed to your environment if you want to use this build pattern.
|
||||
*/
|
||||
String jdk_base = '/home/denny/java_jdks/'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-20.0.1-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-20.0.1-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-20.0.1-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-20.0.1-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-20.0.1-full'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-22.0.2-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
|
||||
|
||||
/**
|
||||
* Download URLs to download the JDK as part of the gradle build packaging process
|
||||
*/
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jdk20.0.1+10-"
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/23+38/bellsoft-jdk23+38-"
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2023 Dennis Sheirer
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -28,4 +28,4 @@
|
||||
# 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
|
||||
projectVersion=0.6.1-beta-2
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2023 Dennis Sheirer
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
18
gradlew
vendored
18
gradlew
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2022 Dennis Sheirer
|
||||
# Copyright (C) 2014-2024 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
|
||||
@@ -88,9 +88,6 @@ done
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
@@ -136,10 +133,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -147,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -155,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -200,6 +200,10 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -663,11 +663,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||
{
|
||||
//Attempt to do a fully qualified identifier match only
|
||||
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||
{
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.getFullyQualifiedTalkgroupAddress());
|
||||
}
|
||||
|
||||
//Attempt to match the talkgroup value
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||
@@ -676,6 +678,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, match the talkgroup to any talkgroup ranges
|
||||
for(Map.Entry<TalkgroupRange, Alias> entry : mTalkgroupRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
@@ -780,11 +783,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(RadioIdentifier identifier)
|
||||
{
|
||||
//Match fully qualified identifier only.
|
||||
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||
{
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.getFullyQualifiedRadioAddress());
|
||||
}
|
||||
|
||||
//Attempt to match against the radio identifier
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mRadioAliasMap.get(value);
|
||||
@@ -793,6 +798,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, attempt to match the radio address against any radio ranges.
|
||||
for(Map.Entry<RadioRange, Alias> entry : mRadioRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Collection;
|
||||
@@ -68,6 +69,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
||||
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mEncrypted = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
||||
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
||||
@@ -129,6 +131,25 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment contains encrypted audio.
|
||||
*
|
||||
* @return encrypted property
|
||||
*/
|
||||
public BooleanProperty encryptedProperty()
|
||||
{
|
||||
return mEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is encrypted
|
||||
* @return true if encrypted.
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return mEncrypted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete property is used by the audio segment producer to signal that the segment is complete and no
|
||||
* additional audio or identifiers will be added to the segment.
|
||||
@@ -386,6 +407,11 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
throw new IllegalStateException("Can't add audio to an audio segment that is being disposed");
|
||||
}
|
||||
|
||||
if(mAudioBuffers.isEmpty())
|
||||
{
|
||||
mStartTimestamp = System.currentTimeMillis() - 20;
|
||||
}
|
||||
|
||||
mAudioBuffers.add(audioBuffer);
|
||||
mSampleCount += audioBuffer.length;
|
||||
}
|
||||
@@ -415,6 +441,14 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
mIdentifierCollection.update(identifier);
|
||||
|
||||
/**
|
||||
* If we have a late-add encryption key, set the encrypted flag to true.
|
||||
*/
|
||||
if(identifier instanceof EncryptionKeyIdentifier eki)
|
||||
{
|
||||
mEncrypted.set(eki.isEncrypted());
|
||||
}
|
||||
|
||||
List<Alias> aliases = mAliasList.getAliases(identifier);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,12 +28,9 @@ 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.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
|
||||
import io.github.dsheirer.preference.duplicate.ICallManagementProvider;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -41,7 +38,10 @@ import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
|
||||
@@ -52,18 +52,41 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
|
||||
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
|
||||
private ICallManagementProvider mCallManagementProvider;
|
||||
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
|
||||
protected Listener<AudioSegment> mDuplicateCallDetectionListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param userPreferences to access the duplicate call detection preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(UserPreferences userPreferences)
|
||||
{
|
||||
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
this(userPreferences.getCallManagementPreference());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param callManagementProvider to provide call management preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(ICallManagementProvider callManagementProvider)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional listener to be notified each time an audio segment is flagged as duplicate.
|
||||
* @param listener to register
|
||||
*/
|
||||
public void setDuplicateCallDetectionListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mDuplicateCallDetectionListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionEnabled())
|
||||
{
|
||||
Identifier identifier = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
|
||||
@@ -78,7 +101,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
|
||||
if(detector == null)
|
||||
{
|
||||
detector = new SystemDuplicateCallDetector();
|
||||
detector = new SystemDuplicateCallDetector(mCallManagementProvider, system);
|
||||
mDetectorMap.put(system, detector);
|
||||
}
|
||||
|
||||
@@ -88,45 +111,99 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System level duplicate call detector. Uses a scheduled executor to run every 25 ms to compare all ongoing call
|
||||
* audio segments to detect duplicates.
|
||||
*
|
||||
* All audio segments remain in the queue until they are flagged as complete. While in the queue, each call is
|
||||
* compared against the others to detect duplicates. Once all calls are either flagged as complete or flagged as
|
||||
* duplicate and removed, the queue is empty and the monitoring is shutdown until a new audio segment arrives and
|
||||
* then the monitoring starts again.
|
||||
*/
|
||||
public class SystemDuplicateCallDetector
|
||||
{
|
||||
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private AtomicBoolean mMonitoring = new AtomicBoolean();
|
||||
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
private boolean mMonitoring = false;
|
||||
private final ICallManagementProvider mCallManagementProvider;
|
||||
private String mSystem;
|
||||
|
||||
public SystemDuplicateCallDetector()
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param callManagementProvider to check for duplicate monitoring preferences
|
||||
*/
|
||||
public SystemDuplicateCallDetector(ICallManagementProvider callManagementProvider, String system)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the audio segment to the monitoring queue.
|
||||
* @param audioSegment to add
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
//Block on audio segment queue so that we don't interfere with monitoring shutdown
|
||||
synchronized(mAudioSegmentQueue)
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mAudioSegmentQueue.add(audioSegment);
|
||||
startMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(false, true))
|
||||
finally
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the call monitoring thread if it's not already running.
|
||||
*
|
||||
* Note: this method should only be called from a thread with the lock acquired.
|
||||
*/
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(!mMonitoring)
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process,
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mMonitoring = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the call monitoring thread if the audio segements queue is empty.
|
||||
*
|
||||
* Note: this should only be called from a separate thread and not from within the scheduled monitoring thread
|
||||
* because cancelling the scheduled timer from within the process() method would kill the thread without
|
||||
* releasing the lock, causing a deadlock.
|
||||
*/
|
||||
private void stopMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(true, false))
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mProcessorFuture != null)
|
||||
//Recheck the audio segments queue to make sure we didn't slip in another audio segment before we can
|
||||
//shut down the scheduled monitoring thread.
|
||||
if(mMonitoring && mAudioSegments.isEmpty())
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
mProcessorFuture = null;
|
||||
}
|
||||
|
||||
mMonitoring = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +214,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
{
|
||||
//Step 1 check for duplicate TO values
|
||||
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
@@ -149,7 +226,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByRadioEnabled())
|
||||
{
|
||||
//Step 2 check for duplicate FROM values
|
||||
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
|
||||
@@ -169,53 +246,51 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
* @param identifiers2
|
||||
* @return
|
||||
*/
|
||||
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
public static boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
{
|
||||
for(Identifier identifier1: identifiers1)
|
||||
{
|
||||
if(identifier1 instanceof TalkgroupIdentifier)
|
||||
if(identifier1 instanceof TalkgroupIdentifier tgId1)
|
||||
{
|
||||
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
|
||||
int tg1 = tgId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof PatchGroupIdentifier)
|
||||
else if(identifier1 instanceof PatchGroupIdentifier pgId1)
|
||||
{
|
||||
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
|
||||
int talkgroup1 = pgId1.getValue().getPatchGroup().getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof RadioIdentifier)
|
||||
else if(identifier1 instanceof RadioIdentifier raId1)
|
||||
{
|
||||
int radio1 = ((RadioIdentifier)identifier1).getValue();
|
||||
int radio1 = raId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
|
||||
if(identifier2 instanceof RadioIdentifier raId2 && raId2.getValue() == radio1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -231,6 +306,8 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private void process()
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//Transfer in newly arrived audio segments
|
||||
@@ -248,6 +325,19 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
return complete;
|
||||
});
|
||||
|
||||
//Remove any encrypted audio segments.
|
||||
mAudioSegments.removeIf(audioSegment -> {
|
||||
boolean encrypted = audioSegment.isEncrypted();
|
||||
|
||||
if(encrypted)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
|
||||
return encrypted;
|
||||
});
|
||||
|
||||
//Only check for duplicates if there is more than one call
|
||||
if(mAudioSegments.size() > 1)
|
||||
{
|
||||
@@ -258,7 +348,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
AudioSegment current = mAudioSegments.get(currentIndex);
|
||||
|
||||
if(!current.isDuplicate())
|
||||
if(current.hasAudio() && !current.isDuplicate())
|
||||
{
|
||||
int checkIndex = currentIndex + 1;
|
||||
|
||||
@@ -273,6 +363,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
toCheck.setDuplicate(true);
|
||||
toCheck.decrementConsumerCount();
|
||||
duplicates.add(toCheck);
|
||||
|
||||
//Notify optional listener that we flagged the call as duplicate.
|
||||
if(mDuplicateCallDetectionListener != null)
|
||||
{
|
||||
mDuplicateCallDetectionListener.receive(toCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,26 +382,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mAudioSegments.removeAll(duplicates);
|
||||
}
|
||||
|
||||
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
|
||||
//Finally, if the audio segment queue is now empty, shutdown monitoring until a new segment arrives.
|
||||
//The monitor shutdown method has to be called on a separate thread so that we don't kill our current
|
||||
// thread and fail to release the lock.
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
|
||||
//allow the add(segment) to restart monitoring as soon as needed.
|
||||
synchronized(mAudioSegmentQueue)
|
||||
{
|
||||
if(mAudioSegmentQueue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
stopMonitoring();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
|
||||
//Do nothing, we got interrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadPool.CACHED.submit(this::stopMonitoring);
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
@@ -313,6 +395,10 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mLog.error("Unknown error while processing audio segments for duplicate call detection. Please report " +
|
||||
"this to the developer.", t);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private T mBroadcastConfiguration;
|
||||
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
|
||||
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
protected int mStreamedAudioCount = 0;
|
||||
protected int mErrorAudioCount = 0;
|
||||
protected int mAgedOffAudioCount = 0;
|
||||
@@ -55,6 +56,14 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable last bad broadcast state property
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current state of the broadcastAudio connection
|
||||
*/
|
||||
@@ -68,10 +77,26 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
*/
|
||||
public void setBroadcastState(BroadcastState broadcastState)
|
||||
{
|
||||
if(broadcastState == BroadcastState.CONNECTED)
|
||||
{
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
else if(broadcastState.isErrorState() || broadcastState.isWarningState())
|
||||
{
|
||||
mLastBadBroadcastState.setValue(broadcastState);
|
||||
}
|
||||
mBroadcastState.setValue(broadcastState);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad state of the broadcastAudio connection
|
||||
*/
|
||||
public BroadcastState getLastBadBroadcastState()
|
||||
{
|
||||
return mLastBadBroadcastState.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the broadcaster
|
||||
*/
|
||||
|
||||
@@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
|
||||
super.setBroadcastState(state);
|
||||
|
||||
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState())
|
||||
if(mBroadcastState.get() != null)
|
||||
{
|
||||
stop();
|
||||
if(mBroadcastState.get().isErrorState())
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
stop();
|
||||
}
|
||||
else if(mBroadcastState.get().isWarningState())
|
||||
{
|
||||
mLog.warn("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if(!connected())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,25 +19,34 @@
|
||||
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
|
||||
@@ -127,7 +136,7 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -138,29 +147,60 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
|
||||
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences);
|
||||
IdentifierCollection identifierCollectionCopy =
|
||||
IdentifierCollection identifiers =
|
||||
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
|
||||
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
if(mUserPreferences.getCallManagementPreference()
|
||||
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
|
||||
{
|
||||
//Decompose the patch group into the individual (patched) talkgroups and process the audio
|
||||
//segment for each patched talkgroup.
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
List<Identifier> ids = new ArrayList<>();
|
||||
ids.addAll(patchGroup.getPatchedTalkgroupIdentifiers());
|
||||
ids.addAll(patchGroup.getPatchedRadioIdentifiers());
|
||||
|
||||
//If there are no patched radios/talkgroups, override user preference and stream as a patch group
|
||||
if(ids.isEmpty() || audioSegment.getAliasList() == null)
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
else
|
||||
{
|
||||
AliasList aliasList = audioSegment.getAliasList();
|
||||
|
||||
for(Identifier identifier: ids)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
Set<BroadcastChannel> broadcastChannels = new HashSet<>();
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
broadcastChannels.addAll(alias.getBroadcastChannels());
|
||||
}
|
||||
|
||||
if(!broadcastChannels.isEmpty())
|
||||
{
|
||||
MutableIdentifierCollection decomposedIdentifiers =
|
||||
new MutableIdentifierCollection(identifiers.getIdentifiers());
|
||||
//Remove patch group TO identifier & replace with the patched talkgroup/radio
|
||||
decomposedIdentifiers.remove(Role.TO);
|
||||
decomposedIdentifiers.update(identifier);
|
||||
processAudioSegment(audioSegment, decomposedIdentifiers, broadcastChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +209,40 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an audio segment for streaming by creating a temporary MP3 recording and submitting the recording
|
||||
* to the specific broadcast channel(s).
|
||||
* @param audioSegment to process for streaming
|
||||
* @param identifierCollection to use for the streamed audio recording.
|
||||
* @param broadcastChannels to receive the audio recording
|
||||
*/
|
||||
private void processAudioSegment(AudioSegment audioSegment, IdentifierCollection identifierCollection,
|
||||
Set<BroadcastChannel> broadcastChannels)
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences, identifierCollection);
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
|
||||
audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -344,14 +344,13 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
deleteBroadcaster(configuredBroadcast);
|
||||
}
|
||||
|
||||
AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
final AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
mAliasModel, mUserPreferences);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
configuredBroadcast.setAudioBroadcaster(audioBroadcaster);
|
||||
audioBroadcaster.setListener(mBroadcastEventListener);
|
||||
audioBroadcaster.start();
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getId(), audioBroadcaster);
|
||||
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
@@ -362,6 +361,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||
ThreadPool.CACHED.submit(audioBroadcaster::start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,90 +23,92 @@ public enum BroadcastState
|
||||
/**
|
||||
* General error in configuration that causes the remote server to reject the connection
|
||||
*/
|
||||
CONFIGURATION_ERROR("Configuration Error", true),
|
||||
CONFIGURATION_ERROR("Configuration Error", true, false),
|
||||
|
||||
/**
|
||||
* Connected to the broadcastAudio server and capable of streaming
|
||||
*/
|
||||
CONNECTED("Connected", false),
|
||||
CONNECTED("Connected", false, false),
|
||||
|
||||
/**
|
||||
* Connection interrupted, attempting to reconnect.
|
||||
*/
|
||||
CONNECTING("Connecting", false),
|
||||
CONNECTING("Connecting", false, false),
|
||||
|
||||
/**
|
||||
* Indicates the configuration is disabled
|
||||
*/
|
||||
DISABLED("Disabled", true),
|
||||
DISABLED("Disabled", true, false),
|
||||
|
||||
/**
|
||||
* Disconnected from the broadcastAudio server
|
||||
*/
|
||||
DISCONNECTED("Disconnected", false),
|
||||
DISCONNECTED("Disconnected", false, false),
|
||||
|
||||
/**
|
||||
* Invalid credentials - user name or password
|
||||
*/
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true),
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true, false),
|
||||
|
||||
/**
|
||||
* Invalid mount point
|
||||
*/
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true, false),
|
||||
|
||||
/**
|
||||
* Invalid configuration settings
|
||||
*/
|
||||
INVALID_SETTINGS("Invalid Settings", true),
|
||||
INVALID_SETTINGS("Invalid Settings", true, false),
|
||||
|
||||
/**
|
||||
* Remote server max sources has been exceeded
|
||||
*/
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", false, true),
|
||||
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false, true),
|
||||
|
||||
/**
|
||||
* Network is unavailable or the server address cannot be resolved
|
||||
*/
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false),
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false, false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
*/
|
||||
NO_SERVER("No Server", false),
|
||||
NO_SERVER("No Server", false, false),
|
||||
|
||||
/**
|
||||
* Initial state with no connection attempted.
|
||||
*/
|
||||
READY("Ready", false),
|
||||
READY("Ready", false, false),
|
||||
|
||||
/**
|
||||
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
|
||||
*/
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false, false),
|
||||
|
||||
/**
|
||||
* Unsupported audio format
|
||||
*/
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true, false),
|
||||
|
||||
/**
|
||||
* Unspecified error
|
||||
*/
|
||||
ERROR("Error", true);
|
||||
ERROR("Error", true, false);
|
||||
|
||||
private String mLabel;
|
||||
private boolean mErrorState;
|
||||
private boolean mWarningState;
|
||||
|
||||
BroadcastState(String label, boolean errorState)
|
||||
BroadcastState(String label, boolean errorState, boolean warningState)
|
||||
{
|
||||
mLabel = label;
|
||||
mErrorState = errorState;
|
||||
mWarningState = warningState;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
@@ -118,4 +120,9 @@ public enum BroadcastState
|
||||
{
|
||||
return mErrorState;
|
||||
}
|
||||
|
||||
public boolean isWarningState()
|
||||
{
|
||||
return mWarningState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ConfiguredBroadcast
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AbstractAudioBroadcaster mAudioBroadcaster;
|
||||
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
|
||||
private ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -89,6 +90,14 @@ public class ConfiguredBroadcast
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad broadcast state of the configured audio broadcaster (optional)
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio broadcaster
|
||||
* @param audioBroadcaster to use for this configuration
|
||||
@@ -96,11 +105,13 @@ public class ConfiguredBroadcast
|
||||
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
|
||||
{
|
||||
mBroadcastState.unbind();
|
||||
mLastBadBroadcastState.unbind();
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty());
|
||||
mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,6 +131,7 @@ public class ConfiguredBroadcast
|
||||
{
|
||||
mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR);
|
||||
}
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.audio.broadcast;
|
||||
|
||||
/**
|
||||
* Options for streaming management of patch groups. These options are used when streaming an audio call that is
|
||||
* tagged to a patch group. The audio will either be streamed once and identified as the patch group, or it will
|
||||
* be streamed multiple times, once for each individual patched talkgroup.
|
||||
*/
|
||||
public enum PatchGroupStreamingOption
|
||||
{
|
||||
PATCH_GROUP("Patch Group"),
|
||||
TALKGROUPS("Individual Talkgroups");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label to display
|
||||
*/
|
||||
PatchGroupStreamingOption(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -341,15 +341,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 403 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
case 401: //Unauthorized
|
||||
if(message.toString().contains("Authentication Required"))
|
||||
{
|
||||
mLog.error("Stream [" + getStreamName() + "] - unable to connect - invalid credentials");
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 401 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -401,9 +415,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
disconnect();
|
||||
break;
|
||||
default:
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
|
||||
disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
@@ -69,6 +70,7 @@
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
|
||||
*
|
||||
@@ -115,14 +117,15 @@
|
||||
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>"))
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error connecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
|
||||
@@ -170,8 +173,6 @@
|
||||
* 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()
|
||||
{
|
||||
@@ -183,14 +184,21 @@
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response != null && response == "200")
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error reconnecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.error("Error reconnecting to OpenMHz server [" + response + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
@@ -565,8 +573,8 @@
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
String responseBody = response.body();
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
|
||||
if (response.statusCode() == 200)
|
||||
{
|
||||
return "OK";
|
||||
@@ -583,8 +591,16 @@
|
||||
return "No Response";
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return e.getLocalizedMessage();
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
return "No Response";
|
||||
}
|
||||
|
||||
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
|
||||
return "Uknown Exception";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ public class MBECallSequenceConverter
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -58,6 +59,7 @@ import javax.sound.sampled.SourceDataLine;
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
@@ -82,6 +84,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
private boolean mDropDuplicates;
|
||||
private long mOutputLastTimestamp = 0;
|
||||
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
|
||||
private AudioFormat mAudioFormat;
|
||||
private Line.Info mLineInfo;
|
||||
private int mRequestedBufferSize;
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
@@ -105,11 +110,14 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + mixerChannel.name()));
|
||||
mUserPreferences = userPreferences;
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mAudioFormat = audioFormat;
|
||||
mLineInfo = lineInfo;
|
||||
mRequestedBufferSize = requestedBufferSize;
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine) mMixer.getLine(lineInfo);
|
||||
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
@@ -230,7 +238,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
}
|
||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||
{
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +274,12 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
private void playAudio(ByteBuffer buffer)
|
||||
{
|
||||
if(mOutput == null)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("null output", 5, "Audio Output is null - ignoring audio playback request");
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer != null)
|
||||
{
|
||||
int wrote = 0;
|
||||
@@ -286,6 +300,103 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
mOutputLastTimestamp = System.currentTimeMillis();
|
||||
|
||||
checkStart();
|
||||
|
||||
//Something is causing the source data line to fail to accept audio byte data via the write() method
|
||||
//and this seems to have started around JDK22, maybe. We can detect when this happens and close() and
|
||||
//reopen the data line to clear the error.
|
||||
if(!mOutput.isRunning() && wrote <= 0 && buffer.array().length > 0)
|
||||
{
|
||||
mOutput.close();
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine)mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output success", 5,
|
||||
"Closed and reopened audio output - success - mOutput is not null");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output fail", 5,
|
||||
"Closed and reopened audio output - fail - mOutput is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException lue)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("reopen fail for lua", 3, "Attempt to reopen " +
|
||||
"audio source data line failed", lue);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mOutput.open(mAudioFormat, mRequestedBufferSize);
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
|
||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
||||
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 3,
|
||||
"Failed to (re)open the source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -120,7 +120,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
while(newSegment != null)
|
||||
{
|
||||
if(newSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
newSegment.decrementConsumerCount();
|
||||
}
|
||||
@@ -148,7 +148,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -182,7 +182,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -228,7 +228,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -307,8 +307,8 @@ public class BinaryMessage extends BitSet
|
||||
|
||||
/**
|
||||
* Overrides the parent method which returns a BitSet so that we can return a BinaryMessage
|
||||
* @param from
|
||||
* @param to
|
||||
* @param from inclusive
|
||||
* @param to exclusive
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@@ -451,6 +451,203 @@ public class BinaryMessage extends BitSet
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(IntField intField)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index = intField.start(); index <= intField.end(); index++)
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument.
|
||||
* @param fragmentedField with an array of message indices
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(FragmentedIntField fragmentedField)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(IntField intField)
|
||||
{
|
||||
return nextSetBit(intField.start()) <= intField.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param fragmentedField to inspect
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(FragmentedIntField fragmentedField)
|
||||
{
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
if(get(index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @param offset to the start of the first message index.
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(IntField intField, int offset)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param fragmentedField with an array of field indices
|
||||
* @param offset to the start of the first message index.
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(FragmentedIntField fragmentedField, int offset)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index + offset))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @param offset to the first bit of the message.
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(IntField intField, int offset)
|
||||
{
|
||||
return nextSetBit(intField.start() + offset) <= (intField.end() + offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @param offset to the first bit of the message.
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(FragmentedIntField fragmentedField, int offset)
|
||||
{
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
if(get(index + offset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the long value of the message field described by the field argument.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @return integer field value.
|
||||
*/
|
||||
public long getLong(LongField intField)
|
||||
{
|
||||
long value = 0;
|
||||
|
||||
for(int index = intField.start(); index <= intField.end(); index++)
|
||||
{
|
||||
value = Long.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the long value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @param offset to the start of the first message index.
|
||||
* @return field value.
|
||||
*/
|
||||
public long getLong(LongField intField, int offset)
|
||||
{
|
||||
long value = 0;
|
||||
|
||||
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
|
||||
{
|
||||
value = Long.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the integer value represented by the bit array
|
||||
*
|
||||
@@ -742,6 +939,18 @@ public class BinaryMessage extends BitSet
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer field formatted as a hex value using zero prefixes to pad the hex character count to fully
|
||||
* represent the size (width) of the field.
|
||||
* @param field to parse as hex
|
||||
* @return hex value.
|
||||
*/
|
||||
public String getHex(IntField field)
|
||||
{
|
||||
int width = Math.ceilDiv(field.width(), 4);
|
||||
return String.format("%0" + width + "X", getInt(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
|
||||
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
|
||||
@@ -848,14 +1057,14 @@ public class BinaryMessage extends BitSet
|
||||
if(get(start))
|
||||
{
|
||||
//Negative value - flip and add one
|
||||
BinaryMessage fragment = getSubMessage(start, end);
|
||||
BinaryMessage fragment = getSubMessage(start, end + 1);
|
||||
fragment.flip(0, fragment.size());
|
||||
return fragment.getInt(1, fragment.size()) + 1;
|
||||
return -(fragment.getInt(0, fragment.size() - 1) + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Positive value - return the contents.
|
||||
return getInt(start + 1, end);
|
||||
return getInt(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,9 +1407,22 @@ public class BinaryMessage extends BitSet
|
||||
{
|
||||
byte[] bytes = new byte[characterCount];
|
||||
|
||||
int index;
|
||||
for(int x = 0; x < characterCount; x++)
|
||||
{
|
||||
bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset);
|
||||
index = x * 7 + offset;
|
||||
|
||||
if(index == 0)
|
||||
{
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, index);
|
||||
bytes[x] = (byte)((bytes[x] >> 1) & 0x7F);
|
||||
}
|
||||
else
|
||||
{
|
||||
index--;
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, index);
|
||||
bytes[x] &= (byte)0x7F; //mask off the high bit since we have to parse it as an 8-bit byte.
|
||||
}
|
||||
}
|
||||
|
||||
return new String(bytes);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
@@ -84,9 +81,9 @@ public class CorrectedBinaryMessage extends BinaryMessage
|
||||
/**
|
||||
* Returns a new binary message containing the bits from (inclusive) to end (exclusive).
|
||||
*
|
||||
* @param start bit
|
||||
* @param end bit
|
||||
* @return message
|
||||
* @param start bit inclusive
|
||||
* @param end bit exclusive
|
||||
* @return message with sub message bits
|
||||
*/
|
||||
public CorrectedBinaryMessage getSubMessage(int start, int end)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.bits;
|
||||
|
||||
/**
|
||||
* Defines a fragmented or non-contiguous bit field within a binary message.
|
||||
* @param indices for the bits in the field.
|
||||
*/
|
||||
public record FragmentedIntField(int... indices)
|
||||
{
|
||||
public FragmentedIntField
|
||||
{
|
||||
if(indices.length > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field indices size [" + indices.length + "] cannot exceed 32-bits for an integer");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param indices (inclusive)
|
||||
* @return constructed fragmented integer field.
|
||||
*/
|
||||
public static FragmentedIntField of(int... indices)
|
||||
{
|
||||
return new FragmentedIntField(indices);
|
||||
}
|
||||
}
|
||||
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal file
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.bits;
|
||||
|
||||
/**
|
||||
* Defines a contiguous bit field within a binary message.
|
||||
* @param start bit index, inclusive.
|
||||
* @param end bit index, inclusive.
|
||||
*/
|
||||
public record IntField(int start, int end)
|
||||
{
|
||||
public IntField
|
||||
{
|
||||
if(end - start > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field length [" + (end - start) + "] cannot exceed 32-bits for an integer");
|
||||
}
|
||||
|
||||
if(end < start)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field start index must be less than end index");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the field in bit positions
|
||||
* @return width
|
||||
*/
|
||||
public int width()
|
||||
{
|
||||
return end() - start() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param start index (inclusive)
|
||||
* @param end index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField range(int start, int end)
|
||||
{
|
||||
return new IntField(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length4(int start)
|
||||
{
|
||||
return new IntField(start, (start + 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length8(int start)
|
||||
{
|
||||
return new IntField(start, (start + 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 12 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length12(int start)
|
||||
{
|
||||
return new IntField(start, (start + 11));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length16(int start)
|
||||
{
|
||||
return new IntField(start, (start + 15));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 20 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length20(int start)
|
||||
{
|
||||
return new IntField(start, (start + 19));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length24(int start)
|
||||
{
|
||||
return new IntField(start, (start + 23));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length32(int start)
|
||||
{
|
||||
return new IntField(start, (start + 31));
|
||||
}
|
||||
}
|
||||
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal file
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.bits;
|
||||
|
||||
/**
|
||||
* Defines a contiguous bit field within a binary message.
|
||||
* @param start bit index, inclusive.
|
||||
* @param end bit index, inclusive.
|
||||
*/
|
||||
public record LongField(int start, int end)
|
||||
{
|
||||
public LongField
|
||||
{
|
||||
if(end - start > 64)
|
||||
{
|
||||
throw new IllegalArgumentException("Long field length [" + (end - start) + "] cannot exceed 64-bits for a long");
|
||||
}
|
||||
|
||||
if(end < start)
|
||||
{
|
||||
throw new IllegalArgumentException("Long field start index must be less than end index");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param start index (inclusive)
|
||||
* @param end index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField range(int start, int end)
|
||||
{
|
||||
return new LongField(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length4(int start)
|
||||
{
|
||||
return new LongField(start, (start + 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length8(int start)
|
||||
{
|
||||
return new LongField(start, (start + 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 12 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length12(int start)
|
||||
{
|
||||
return new LongField(start, (start + 11));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length16(int start)
|
||||
{
|
||||
return new LongField(start, (start + 15));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 20 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length20(int start)
|
||||
{
|
||||
return new LongField(start, (start + 19));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length24(int start)
|
||||
{
|
||||
return new LongField(start, (start + 23));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length32(int start)
|
||||
{
|
||||
return new LongField(start, (start + 31));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.channel.metadata;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
@@ -35,11 +36,10 @@ import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier
|
||||
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Channel metadata containing details about the channel configuration, decoder state and current
|
||||
@@ -58,6 +58,7 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
private DecoderTypeConfigurationIdentifier mDecoderTypeConfigurationIdentifier;
|
||||
private Identifier mFromIdentifier;
|
||||
private List<Alias> mFromIdentifierAliases;
|
||||
private Identifier mTalkerAliasIdentifier;
|
||||
private Identifier mToIdentifier;
|
||||
private List<Alias> mToIdentifierAliases;
|
||||
private Integer mTimeslot;
|
||||
@@ -66,17 +67,51 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
private AliasModel mAliasModel;
|
||||
private AliasList mAliasList;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param aliasModel for alias lookups
|
||||
* @param timeslot for this metadata
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel, Integer timeslot)
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param aliasModel for alias lookups
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel)
|
||||
{
|
||||
this(aliasModel, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a textual description of this channel metadata.
|
||||
*/
|
||||
public String getDescription()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Channel Metadata Description\n");
|
||||
sb.append("\tTimeslot: ").append(mTimeslot).append("\n");
|
||||
Identifier decoder = getDecoderTypeConfigurationIdentifier();
|
||||
sb.append("\tDecoder: ").append(decoder != null ? decoder : "(null)").append("\n");
|
||||
Identifier state = getChannelStateIdentifier();
|
||||
sb.append("\tState: ").append(state != null ? state : "(null)").append("\n");
|
||||
Identifier system = getSystemConfigurationIdentifier();
|
||||
sb.append("\tSystem: ").append(system != null ? system : "(null)").append("\n");
|
||||
Identifier site = getSiteConfigurationIdentifier();
|
||||
sb.append("\tSite: ").append(site != null ? site : "(null)").append("\n");
|
||||
Identifier channel = getChannelNameConfigurationIdentifier();
|
||||
sb.append("\tChannel: ").append(channel != null ? channel : "(null)").append("\n");
|
||||
Identifier frequency = getFrequencyConfigurationIdentifier();
|
||||
sb.append("\tFrequency: ").append(frequency != null ? frequency : "(null)").append("\n");
|
||||
Identifier logical = getDecoderLogicalChannelNameIdentifier();
|
||||
sb.append("\tLogical Channel: ").append(logical != null ? logical : "(null)").append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Integer getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
@@ -205,6 +240,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
return mFromIdentifierAliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional talker alias identifier
|
||||
*/
|
||||
public Identifier getTalkerAliasIdentifier()
|
||||
{
|
||||
return mTalkerAliasIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if we have a non-null talker alias identifier
|
||||
*/
|
||||
public boolean hasTalkerAliasIdentifier()
|
||||
{
|
||||
return mTalkerAliasIdentifier != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current call event TO identifier
|
||||
*/
|
||||
@@ -376,15 +427,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
case USER:
|
||||
if(identifier.getRole() == Role.FROM)
|
||||
{
|
||||
mFromIdentifier = update.isAdd() ? identifier : null;
|
||||
|
||||
if(mAliasList != null && mFromIdentifier != null)
|
||||
if(identifier.getForm() == Form.TALKER_ALIAS)
|
||||
{
|
||||
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
|
||||
mTalkerAliasIdentifier = identifier;
|
||||
}
|
||||
else
|
||||
{
|
||||
mFromIdentifierAliases = Collections.EMPTY_LIST;
|
||||
mFromIdentifier = update.isAdd() ? identifier : null;
|
||||
|
||||
if(mAliasList != null && mFromIdentifier != null)
|
||||
{
|
||||
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFromIdentifierAliases = Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
broadcastUpdate(ChannelMetadataField.USER_FROM);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.channel.metadata;
|
||||
|
||||
@@ -29,16 +26,16 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener
|
||||
{
|
||||
@@ -84,6 +81,47 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diagnostic description of the channel metadata and metadata-channel map.
|
||||
* @return diagnostic description.
|
||||
*/
|
||||
public String getDiagnosticInformation()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Map<ChannelMetadata,Channel> mapCopy = new HashMap<>(mMetadataChannelMap);
|
||||
List<ChannelMetadata> metadataCopy = new ArrayList<>(mChannelMetadata);
|
||||
|
||||
sb.append("Channel Metadata Model Diagnostics\n");
|
||||
|
||||
for(ChannelMetadata channelMetadata: metadataCopy)
|
||||
{
|
||||
sb.append("\n--------------- CHANNEL METADATA ITEM --------------------\n\n");
|
||||
if(mapCopy.containsKey(channelMetadata))
|
||||
{
|
||||
Channel channel = mapCopy.get(channelMetadata);
|
||||
sb.append("\tMapped Channel:").append(channel).append("\n");
|
||||
sb.append("\t\tSource Configuration: ").append(channel.getSourceConfiguration()).append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("\tMapped Channel: **Channel Metadata Is Not In the Metadata:Channel Map**\n");
|
||||
}
|
||||
|
||||
sb.append(channelMetadata.getDescription());
|
||||
}
|
||||
|
||||
//Check for orphaned map entries that don't match the current channel metadata list.
|
||||
for(Map.Entry<ChannelMetadata,Channel> entry: mapCopy.entrySet())
|
||||
{
|
||||
if(!metadataCopy.contains(entry.getKey()))
|
||||
{
|
||||
sb.append("\n\tWARNING: orphaned Metadata Channel map entry - channel: " + entry.getValue()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
@@ -241,7 +279,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
else
|
||||
{
|
||||
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
|
||||
String value = id.getValue().replace(" TS1", "")
|
||||
.replace(" TS2", "");
|
||||
return value + " TS:" + channelMetadata.getTimeslot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -38,6 +38,14 @@ import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -52,14 +60,6 @@ import javax.swing.SwingConstants;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
|
||||
{
|
||||
@@ -294,6 +294,10 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
text = EMPTY_VALUE;
|
||||
}
|
||||
else if(hasAdditionalIdentifier(channelMetadata))
|
||||
{
|
||||
text = text + " " + getAdditionalIdentifier(channelMetadata);
|
||||
}
|
||||
|
||||
label.setText(text);
|
||||
}
|
||||
@@ -306,6 +310,8 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
}
|
||||
|
||||
public abstract Identifier getIdentifier(ChannelMetadata channelMetadata);
|
||||
public abstract boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata);
|
||||
public abstract Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +329,18 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
return channelMetadata.getFromIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata)
|
||||
{
|
||||
return channelMetadata.getTalkerAliasIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata)
|
||||
{
|
||||
return channelMetadata.hasTalkerAliasIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,6 +358,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
return channelMetadata.getToIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata) {return null;}
|
||||
@Override
|
||||
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata) {return false;}
|
||||
}
|
||||
|
||||
public class ColoredStateCellRenderer extends DefaultTableCellRenderer
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.channel.state;
|
||||
@@ -29,6 +26,7 @@ import io.github.dsheirer.message.IMessageListener;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.ActivitySummaryProvider;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -47,9 +45,38 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
protected Broadcaster<IDecodeEvent> mDecodeEventBroadcaster = new Broadcaster<>();
|
||||
protected Listener<DecoderStateEvent> mDecoderStateListener;
|
||||
private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener();
|
||||
private static final DecodeEventDuplicateDetector mDuplicateEventDetector = new DecodeEventDuplicateDetector();
|
||||
private boolean mRunning;
|
||||
|
||||
public abstract DecoderType getDecoderType();
|
||||
|
||||
/**
|
||||
* Implements module start and sets the mRunning flag to true so that messages can be processed.
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
mRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the module stop and sets the mRunning flag to false to stop message processing
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this module is running and can process/pass messages down to sub-class implementations.
|
||||
* @return true if running
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides subclass reference to the decode event broadcaster
|
||||
*/
|
||||
@@ -80,7 +107,10 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
*/
|
||||
protected void broadcast(IDecodeEvent event)
|
||||
{
|
||||
mDecodeEventBroadcaster.broadcast(event);
|
||||
if(!mDuplicateEventDetector.isDuplicate(event, System.currentTimeMillis()))
|
||||
{
|
||||
mDecodeEventBroadcaster.broadcast(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +166,9 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
mDecoderStateListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper that implements the IDecoderStateEventListener interface
|
||||
*/
|
||||
private class DecoderStateEventListener implements Listener<DecoderStateEvent>
|
||||
{
|
||||
@Override
|
||||
@@ -144,5 +177,4 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
receiveDecoderStateEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.channel.state;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Basic decoder channel state - provides the minimum channel state functionality
|
||||
* to support an always un-squelched audio decoder.
|
||||
*/
|
||||
public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
||||
private static final String NO_SQUELCH = "No Squelch";
|
||||
private Identifier mChannelNameIdentifier;
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
private String mChannelName;
|
||||
|
||||
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
|
||||
{
|
||||
mDecoderType = decoderType;
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL";
|
||||
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder:\t");
|
||||
sb.append(mDecoderType);
|
||||
sb.append("\n\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(IMessage t)
|
||||
{
|
||||
/* Not implemented */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
if(event.getEvent() == Event.REQUEST_RESET)
|
||||
{
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mDecoderType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, Event.REQUEST_ALWAYS_UNSQUELCH, State.IDLE));
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
@@ -38,13 +39,21 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
private MutableIdentifierCollection mIdentifierCollection;
|
||||
protected Listener<IdentifierUpdateNotification> mConfigurationIdentifierListener;
|
||||
protected IChannelDescriptor mCurrentChannel;
|
||||
private long mCurrentFrequency;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param mutableIdentifierCollection to preload into this decoder state
|
||||
*/
|
||||
public DecoderState(MutableIdentifierCollection mutableIdentifierCollection)
|
||||
{
|
||||
mIdentifierCollection = mutableIdentifierCollection;
|
||||
mIdentifierCollection.update(new DecoderTypeConfigurationIdentifier(getDecoderType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance using an empty identifier collection.
|
||||
*/
|
||||
public DecoderState()
|
||||
{
|
||||
this(new MutableIdentifierCollection());
|
||||
@@ -54,10 +63,28 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
super.start();
|
||||
//Broadcast the existing identifiers (as add events) so that they can be received by external listeners
|
||||
mIdentifierCollection.broadcastIdentifiers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current frequency for this channel.
|
||||
*/
|
||||
public long getCurrentFrequency()
|
||||
{
|
||||
return mCurrentFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current frequency for this channel.
|
||||
* @param frequency to set.
|
||||
*/
|
||||
public void setCurrentFrequency(long frequency)
|
||||
{
|
||||
mCurrentFrequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive identifier update notifications
|
||||
*/
|
||||
@@ -95,7 +122,7 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
/**
|
||||
* Sets the current channel descriptor
|
||||
*/
|
||||
protected void setCurrentChannel(IChannelDescriptor channel)
|
||||
public void setCurrentChannel(IChannelDescriptor channel)
|
||||
{
|
||||
mCurrentChannel = channel;
|
||||
}
|
||||
@@ -152,9 +179,13 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
{
|
||||
getIdentifierCollection().receive(identifierUpdateNotification);
|
||||
|
||||
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier)
|
||||
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier cdci)
|
||||
{
|
||||
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifierUpdateNotification.getIdentifier()).getValue());
|
||||
setCurrentChannel(cdci.getValue());
|
||||
}
|
||||
else if(identifierUpdateNotification.getIdentifier() instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
setCurrentFrequency(fci.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,12 +26,20 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Base decoder state implementation for a multi-timeslot protocol that tracks the state for a single timeslot.
|
||||
*/
|
||||
public abstract class TimeslotDecoderState extends DecoderState
|
||||
{
|
||||
private int mTimeslot;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param timeslot to monitor/maintain state.
|
||||
*/
|
||||
public TimeslotDecoderState(int timeslot)
|
||||
{
|
||||
super(new MutableIdentifierCollection(timeslot));
|
||||
@@ -39,6 +47,9 @@ public abstract class TimeslotDecoderState extends DecoderState
|
||||
mConfigurationIdentifierListener = new TimeslotConfigurationIdentifierListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitored timeslot for this decoder state instance.
|
||||
*/
|
||||
protected int getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
@@ -82,6 +93,10 @@ public abstract class TimeslotDecoderState extends DecoderState
|
||||
{
|
||||
setCurrentChannel((IChannelDescriptor)identifier);
|
||||
}
|
||||
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
setCurrentFrequency(fci.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
@@ -35,13 +32,16 @@ import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigFactory;
|
||||
import io.github.dsheirer.source.config.SourceConfigRecording;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import java.beans.Transient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -55,9 +55,6 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.beans.Transient;
|
||||
import java.util.Objects;
|
||||
|
||||
@JacksonXmlRootElement(localName = "channel")
|
||||
public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
{
|
||||
@@ -88,7 +85,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
private BooleanProperty mAutoStart = new SimpleBooleanProperty();
|
||||
private IntegerProperty mAutoStartOrder = new SimpleIntegerProperty();
|
||||
private boolean mSelected;
|
||||
private TunerChannel mTunerChannel = null;
|
||||
private List<TunerChannel> mTunerChannels = null;
|
||||
|
||||
private ChannelType mChannelType = ChannelType.STANDARD;
|
||||
|
||||
@@ -634,9 +631,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
|
||||
updateFrequencies();
|
||||
|
||||
//Clear the tune channel object so that it can be recreated if the
|
||||
//source configuration changes
|
||||
mTunerChannel = null;
|
||||
//Clear the tune channels object so that it can be recreated if the source configuration changes
|
||||
mTunerChannels = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,35 +676,50 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to construct a tuner channel object representing a
|
||||
* tuner or recording source frequency and bandwidth that can be used by
|
||||
* application components for graphically representing this channel.
|
||||
* Convenience method to construct a tuner channel object representing a tuner or recording source frequency and
|
||||
* bandwidth that can be used by application components for graphically representing this channel.
|
||||
*
|
||||
* If the source configuration is not a tuner or recording, this method
|
||||
* returns null.
|
||||
* If the source configuration is not a tuner or recording, this method returns an empty list.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public TunerChannel getTunerChannel()
|
||||
public List<TunerChannel> getTunerChannels()
|
||||
{
|
||||
if(mTunerChannel == null)
|
||||
if(mTunerChannels == null && mSourceConfiguration != null)
|
||||
{
|
||||
if(mSourceConfiguration.getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
|
||||
mTunerChannels = new ArrayList<>();
|
||||
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth());
|
||||
}
|
||||
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
|
||||
switch(mSourceConfiguration.getSourceType())
|
||||
{
|
||||
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth());
|
||||
case TUNER:
|
||||
if(mSourceConfiguration instanceof SourceConfigTuner tunerConfig)
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(tunerConfig.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
break;
|
||||
case TUNER_MULTIPLE_FREQUENCIES:
|
||||
if(mSourceConfiguration instanceof SourceConfigTunerMultipleFrequency multiConfig)
|
||||
{
|
||||
for(long frequency: multiConfig.getFrequencies())
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(frequency,
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RECORDING:
|
||||
if(mSourceConfiguration instanceof SourceConfigRecording recordingConfig)
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(recordingConfig.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.warn("Unrecognized channel source type: " + mSourceConfiguration.getSourceType());
|
||||
}
|
||||
}
|
||||
|
||||
return mTunerChannel;
|
||||
return mTunerChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,9 +728,15 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
*/
|
||||
public boolean isWithin(long minimum, long maximum)
|
||||
{
|
||||
TunerChannel tunerChannel = getTunerChannel();
|
||||
for(TunerChannel tunerChannel: getTunerChannels())
|
||||
{
|
||||
if(tunerChannel.overlaps(minimum, maximum))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return tunerChannel != null && tunerChannel.overlaps(minimum, maximum);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
||||
import io.github.dsheirer.channel.state.AbstractChannelState;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
|
||||
@@ -52,6 +53,7 @@ import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -72,9 +74,10 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
{
|
||||
private static final String DIVIDER = "-------------------------------------------------------------------------\n";
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
|
||||
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
||||
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
|
||||
private Map<Channel,ProcessingChain> mProcessingChainsMap = new ConcurrentHashMap<>();
|
||||
private Lock mLock = new ReentrantLock();
|
||||
|
||||
private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener();
|
||||
@@ -131,7 +134,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
isProcessing = mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing();
|
||||
isProcessing = mProcessingChainsMap.containsKey(channel) && mProcessingChainsMap.get(channel).isProcessing();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -147,7 +150,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*/
|
||||
public boolean isProcessing()
|
||||
{
|
||||
return !mProcessingChains.isEmpty();
|
||||
return !mProcessingChainsMap.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +159,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*/
|
||||
public ProcessingChain getProcessingChain(Channel channel)
|
||||
{
|
||||
return mProcessingChains.get(channel);
|
||||
return mProcessingChainsMap.get(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +178,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue() == processingChain)
|
||||
{
|
||||
@@ -206,7 +209,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().hasSource(tunerChannelSource))
|
||||
{
|
||||
@@ -276,7 +279,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
break;
|
||||
case REQUEST_DISABLE:
|
||||
case NOTIFICATION_DELETE:
|
||||
if(channel.isProcessing())
|
||||
if(channel != null && channel.isProcessing())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -379,8 +382,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
String threadName = "sdrtrunk channel [" + channel.getChannelID() + "/" +
|
||||
channel.getDecodeConfiguration().getDecoderType().getShortDisplayString() + "]";
|
||||
source = mTunerManager.getSource(channel.getSourceConfiguration(),
|
||||
channel.getDecodeConfiguration().getChannelSpecification());
|
||||
channel.getDecodeConfiguration().getChannelSpecification(), threadName);
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
@@ -390,7 +395,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
if(source == null)
|
||||
{
|
||||
//This has to be done on the FX event thread when the playlist editor is constructed
|
||||
Platform.runLater(() -> channel.setProcessing(false));
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
Platform.runLater(() -> channel.setProcessing(false));
|
||||
}
|
||||
|
||||
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel,
|
||||
ChannelEvent.Event.NOTIFICATION_PROCESSING_START_REJECTED, TUNER_UNAVAILABLE_DESCRIPTION));
|
||||
@@ -442,7 +450,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
/* Processing Modules */
|
||||
List<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
|
||||
request.getTrafficChannelManager());
|
||||
request.getTrafficChannelManager(), request.getChannelDescriptor());
|
||||
processingChain.addModules(modules);
|
||||
|
||||
//Post preload data from the request to the event bus. Modules that can handle preload data will annotate
|
||||
@@ -556,10 +564,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
if(!mProcessingChains.containsKey(channel))
|
||||
if(!mProcessingChainsMap.containsKey(channel))
|
||||
{
|
||||
added = true;
|
||||
mProcessingChains.put(channel, processingChain);
|
||||
mProcessingChainsMap.put(channel, processingChain);
|
||||
getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
|
||||
}
|
||||
}
|
||||
@@ -584,7 +592,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
removed = mProcessingChains.remove(channel);
|
||||
removed = mProcessingChainsMap.remove(channel);
|
||||
|
||||
if(removed != null)
|
||||
{
|
||||
@@ -633,6 +641,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
processingChain.removeFrequencyChangeListener(channel);
|
||||
channel.resetFrequencyCorrection();
|
||||
|
||||
//Notify all processing chains that this channel is shutting down so that if this is a traffic channel,
|
||||
//the owning parent channel's traffic channel manager can cleanup it's accounting.
|
||||
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
|
||||
mChannelEventBroadcaster.removeListener(processingChain);
|
||||
|
||||
@@ -660,7 +670,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
mDelayedChannelStartTasks.remove(delayedTask);
|
||||
}
|
||||
|
||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
|
||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChainsMap.keySet());
|
||||
|
||||
for(Channel channel : channelsToStop)
|
||||
{
|
||||
@@ -683,7 +693,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
public void convertToTrafficChannel(ChannelConversionRequest request)
|
||||
{
|
||||
//Update the channel to processing chain map.
|
||||
ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel());
|
||||
ProcessingChain processingChain = mProcessingChainsMap.remove(request.getCurrentChannel());
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
@@ -697,7 +707,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
request.getTrafficChannel().setProcessing(true);
|
||||
});
|
||||
|
||||
mProcessingChains.put(request.getTrafficChannel(), processingChain);
|
||||
mProcessingChainsMap.put(request.getTrafficChannel(), processingChain);
|
||||
mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(),
|
||||
request.getTrafficChannel());
|
||||
|
||||
@@ -803,7 +813,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().hasSource(sourceEvent.getSource()))
|
||||
{
|
||||
@@ -841,4 +851,54 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diagnostic report.
|
||||
* @return report text.
|
||||
*/
|
||||
public String getDiagnosticInformation()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Channel Processing Manager - Diagnostics Report\n\n");
|
||||
sb.append(DIVIDER);
|
||||
sb.append("\tChannel to Processing Chain Map Contents\n");
|
||||
Map<Channel,ProcessingChain> mapCopy = new HashMap<>(mProcessingChainsMap);
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mapCopy.entrySet())
|
||||
{
|
||||
sb.append("\n\n--------------- CHANNEL:PROCESSING CHAIN MAP ENTRY --------------------\n");
|
||||
try
|
||||
{
|
||||
Channel channel = entry.getKey();
|
||||
sb.append("\tChannel: " + channel).append("\n");
|
||||
sb.append("\t\tSource Configuration: " + channel.getSourceConfiguration()).append("\n");
|
||||
ProcessingChain chain = entry.getValue();
|
||||
sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n");
|
||||
AbstractChannelState state = chain.getChannelState();
|
||||
sb.append("Channel State: ").append(state.getClass()).append("\n");
|
||||
for(ChannelMetadata metadata: state.getChannelMetadata())
|
||||
{
|
||||
sb.append(metadata.getDescription()).append("\n");
|
||||
}
|
||||
|
||||
Source source = chain.getSource();
|
||||
|
||||
if(source != null)
|
||||
{
|
||||
sb.append("Channel Source Class: " + source.getClass()).append("\n");
|
||||
sb.append("\t\tTo String:").append(source).append("\n");
|
||||
sb.append("\t\tHash:").append(Integer.toHexString(source.hashCode()).toUpperCase()).append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Channel Source: (null)\n");
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
sb.append("\tError while logging diagnostics of map entry - " + t.getMessage()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -147,6 +147,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
sb.append(" REQUESTED CF: ").append(FREQUENCY_FORMAT.format(requestedCenterFrequency / 1E6d));
|
||||
sb.append(" MIXER:").append(FREQUENCY_FORMAT.format(appliedFrequencyOffset / 1E6d));
|
||||
sb.append(" | Polyphase Indices: ").append(indexes);
|
||||
sb.append(" HASH:").append(Integer.toHexString(pcs.hashCode()).toUpperCase());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
@@ -188,9 +189,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
* Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
|
||||
* sourced due to the current center frequency and/or sample rate.
|
||||
* @param tunerChannel specifying center frequency and bandwidth.
|
||||
* @param threadName for the channel's dispatcher
|
||||
* @return source or null.
|
||||
*/
|
||||
public TunerChannelSource getChannel(TunerChannel tunerChannel)
|
||||
public TunerChannelSource getChannel(TunerChannel tunerChannel, String threadName)
|
||||
{
|
||||
PolyphaseChannelSource channelSource = null;
|
||||
|
||||
@@ -199,7 +201,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
try
|
||||
{
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
||||
mChannelSourceEventListener);
|
||||
mChannelSourceEventListener, threadName);
|
||||
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -56,12 +56,14 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
* @param channelCalculator for current channel center frequency and sample rate and index calculations
|
||||
* @param filterManager for access to new or cached synthesis filters
|
||||
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
||||
* @param threadName for the channel's dispatcher
|
||||
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
|
||||
Listener<SourceEvent> producerSourceEventListener) throws IllegalArgumentException
|
||||
Listener<SourceEvent> producerSourceEventListener, String threadName)
|
||||
throws IllegalArgumentException
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel);
|
||||
super(producerSourceEventListener, tunerChannel, threadName);
|
||||
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||
}
|
||||
@@ -222,7 +224,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
{
|
||||
case 1:
|
||||
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, channelCalculator.getChannelCount(), getHeartbeatManager());
|
||||
indexes, channelCalculator.getChannelCount(), getHeartbeatManager(), mThreadName);
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
@@ -233,7 +235,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
|
||||
channelCalculator.getChannelBandwidth(), 2);
|
||||
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager());
|
||||
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager(), mThreadName);
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,16 +41,15 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
* oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
|
||||
*
|
||||
* @param inputChannelCount is the number of input channels for this output processor
|
||||
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
|
||||
* channel sample rate for frequency translation/correction.
|
||||
* @param heartbeatManager to receive pings on the dispatcher thread
|
||||
* @param threadName for the dispatcher
|
||||
*/
|
||||
public ChannelOutputProcessor(int inputChannelCount, double sampleRate, HeartbeatManager heartbeatManager)
|
||||
public ChannelOutputProcessor(int inputChannelCount, HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
mInputChannelCount = inputChannelCount;
|
||||
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
|
||||
mHeartbeatManager = heartbeatManager;
|
||||
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
|
||||
mChannelResultsDispatcher = new Dispatcher(threadName,50, mHeartbeatManager);
|
||||
mChannelResultsDispatcher.setListener(floats -> {
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -38,11 +38,12 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
* @param channelIndexes containing a single channel index.
|
||||
* @param gain value to apply. This is typically the same as the channelizer's channel count.
|
||||
* @param heartbeatManager to receive heartbeats on the dispatch thread
|
||||
* @param threadName to use for this processor
|
||||
*/
|
||||
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain,
|
||||
HeartbeatManager heartbeatManager)
|
||||
HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
super(1, sampleRate, heartbeatManager);
|
||||
super(1, heartbeatManager, threadName);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
mMixerAssembler = new OneChannelMixerAssembler(gain);
|
||||
mMixerAssembler.getMixer().setSampleRate(sampleRate);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -37,15 +37,17 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
*
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing two channel indices.
|
||||
* @param filter to use
|
||||
* @param gain to apply to output. Typically this is equal to the channelizer's channel count.
|
||||
* @param heartbeatManager to be pinged on the dispatcher thread
|
||||
* @param threadName for the dispatcher
|
||||
*/
|
||||
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain,
|
||||
HeartbeatManager heartbeatManager)
|
||||
HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
|
||||
//after synthesizing both input channels
|
||||
super(2, sampleRate, heartbeatManager);
|
||||
super(2, heartbeatManager, threadName);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
mMixerAssembler = new TwoChannelMixerAssembler(gain);
|
||||
mMixerAssembler.getMixer().setSampleRate(sampleRate);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.dsp.oscillator;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -45,7 +46,15 @@ public class VectorRealOscillator extends AbstractOscillator implements IRealOsc
|
||||
@Override
|
||||
public float[] generate(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount];
|
||||
int length = sampleCount;
|
||||
|
||||
//Increase the length until it aligns as a multiple of the vector species length
|
||||
while(length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
float[] samples = new float[length];
|
||||
|
||||
for(int samplePointer = 0; samplePointer < sampleCount; samplePointer += VECTOR_SPECIES.length())
|
||||
{
|
||||
@@ -58,6 +67,7 @@ public class VectorRealOscillator extends AbstractOscillator implements IRealOsc
|
||||
generated.intoArray(samples, samplePointer);
|
||||
}
|
||||
|
||||
return samples;
|
||||
//Truncate the array to the requested length, if we increased it for SIMD alignment.
|
||||
return Arrays.copyOf(samples, sampleCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
@@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory;
|
||||
public class BerlekempMassey
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||
|
||||
/* Golay field size GF( 2 ** MM ) */
|
||||
private int MM;
|
||||
@@ -234,7 +236,16 @@ public class BerlekempMassey
|
||||
/* put recd[i] into index form (ie as powers of alpha) */
|
||||
for(int i = 0; i < NN; i++)
|
||||
{
|
||||
output[i] = index_of[input[i]];
|
||||
try
|
||||
{
|
||||
output[i] = index_of[input[i]];
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error(getClass().toString(), 2, "Reed Solomon Decoder error for " +
|
||||
"class [" + getClass() + "] there may be an issue with the message parser class indices - " +
|
||||
"ensure the hex bit values are not larger 63", e);
|
||||
}
|
||||
}
|
||||
|
||||
/* first form the syndromes */
|
||||
|
||||
285
src/main/java/io/github/dsheirer/edac/Binary40_9_16.java
Normal file
285
src/main/java/io/github/dsheirer/edac/Binary40_9_16.java
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.IntField;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* P25 Phase 2 Binary (40, 9, 16) Code for IISCH in the Super Frame Fragment.
|
||||
*/
|
||||
public class Binary40_9_16
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(Binary40_9_16.class);
|
||||
private static final long[] CHECKSUMS = {0x8816CE36D7l, 0x201DFD4F64l, 0x100F4B1758l, 0x0C00DED18El, 0x020807F7FFl,
|
||||
0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l};
|
||||
private static final long CODE_WORD_OFFSET = 0x184229d461l;
|
||||
private static final Map<Long,BinaryMessage> CODEWORD_MAP_TS1 = new TreeMap<>();
|
||||
private static final Map<Long,BinaryMessage> CODEWORD_MAP_TS2 = new TreeMap<>();
|
||||
private static final IntField CHANNEL_NUMBER = IntField.range(2, 3);
|
||||
private static final IntField ISCH_LOCATION = IntField.range(4, 5);
|
||||
private static final IntField WORD = IntField.range(0, 8);
|
||||
|
||||
public Binary40_9_16()
|
||||
{
|
||||
for(int x = 0; x < 512; x++)
|
||||
{
|
||||
BinaryMessage message = new BinaryMessage(9);
|
||||
message.load(0, 9, x);
|
||||
|
||||
int channelNumber = message.getInt(CHANNEL_NUMBER) + 1;
|
||||
int ischLocation = message.getInt(ISCH_LOCATION);
|
||||
|
||||
long codeword = getCodeWord(message);
|
||||
codeword ^= CODE_WORD_OFFSET;
|
||||
|
||||
if(ischLocation != 3)
|
||||
{
|
||||
if(channelNumber == 1)
|
||||
{
|
||||
CODEWORD_MAP_TS1.put(codeword, message);
|
||||
}
|
||||
else if(channelNumber == 2)
|
||||
{
|
||||
CODEWORD_MAP_TS2.put(codeword, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Long> keys = new ArrayList<>(CODEWORD_MAP_TS1.keySet());
|
||||
Collections.sort(keys);
|
||||
for(Long key: keys)
|
||||
{
|
||||
BinaryMessage message = CODEWORD_MAP_TS1.get(key);
|
||||
int distance = getDistance(key, keys);
|
||||
}
|
||||
List<Long> keys2 = new ArrayList<>(CODEWORD_MAP_TS2.keySet());
|
||||
Collections.sort(keys2);
|
||||
for(Long key: keys2)
|
||||
{
|
||||
BinaryMessage message = CODEWORD_MAP_TS2.get(key);
|
||||
int distance = getDistance(key, keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the closest distance between the value and the values in the value set.
|
||||
* @param value to test
|
||||
* @param valueSet to test against
|
||||
* @return closest distance of the value to the values in the value set, ignoring any equivalent values.
|
||||
*/
|
||||
public static int getDistance(long value, List<Long> valueSet)
|
||||
{
|
||||
int distance = 40;
|
||||
|
||||
for(Long candidate: valueSet)
|
||||
{
|
||||
if(candidate != value)
|
||||
{
|
||||
long syndrome = candidate ^ value;
|
||||
int candidateDistance = Long.bitCount(syndrome);
|
||||
if(candidateDistance < distance)
|
||||
{
|
||||
distance = candidateDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a codeword from the binary message where each set bit in the binary message corresponds to one of the
|
||||
* words in the generator and we XOR the words together.
|
||||
* @param message for generating a codeword
|
||||
* @return codeword
|
||||
*/
|
||||
public static long getCodeWord(BinaryMessage message)
|
||||
{
|
||||
long codeword = 0;
|
||||
|
||||
for(int x = 0; x < message.length(); x++)
|
||||
{
|
||||
if(message.get(x))
|
||||
{
|
||||
codeword ^= CHECKSUMS[x];
|
||||
}
|
||||
}
|
||||
|
||||
return codeword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum (ie codeword) from the set bits in the message.
|
||||
* @param message with set bits, 0-8
|
||||
* @return
|
||||
*/
|
||||
private static long calculateChecksum(BinaryMessage message)
|
||||
{
|
||||
long calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = message.nextSetBit(0); i < 9; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CHECKSUMS[i];
|
||||
}
|
||||
|
||||
return calculated;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Performs error detection and returns a corrected copy of the 24-bit
|
||||
// * message that starts at the start index.
|
||||
// *
|
||||
// * @param message - source message containing startIndex + 24 bits length
|
||||
// * @return - corrected 24-bit galois value
|
||||
// */
|
||||
// public static int checkAndCorrect(CorrectedBinaryMessage message)
|
||||
// {
|
||||
// boolean parityError = message.cardinality() % 2 != 0;
|
||||
//
|
||||
// long syndrome = getSyndrome(message);
|
||||
//
|
||||
// /* No errors */
|
||||
// if(syndrome == 0)
|
||||
// {
|
||||
// if(parityError)
|
||||
// {
|
||||
// message.flip(23);
|
||||
// message.incrementCorrectedBitCount(1);
|
||||
// return 1;
|
||||
// }
|
||||
//
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
// /* Get original message value */
|
||||
// int original = message.getInt(0, 22);
|
||||
//
|
||||
// int index = -1;
|
||||
// int syndromeWeight = 3;
|
||||
// int errors = 0;
|
||||
//
|
||||
// while(index < 23)
|
||||
// {
|
||||
// if(index != -1)
|
||||
// {
|
||||
// /* restore the previous flipped bit */
|
||||
// if(index > 0)
|
||||
// {
|
||||
// message.flip(index - 1);
|
||||
// }
|
||||
//
|
||||
// message.flip(index);
|
||||
//
|
||||
// syndromeWeight = 2;
|
||||
// }
|
||||
//
|
||||
// syndrome = getSyndrome(message);
|
||||
//
|
||||
// if(syndrome > 0)
|
||||
// {
|
||||
// for(int i = 0; i < 23; i++)
|
||||
// {
|
||||
//
|
||||
// errors = Long.bitCount(syndrome);
|
||||
//
|
||||
// if(errors <= syndromeWeight)
|
||||
// {
|
||||
// message.xor(12, 11, syndrome);
|
||||
//
|
||||
// message.rotateRight(i, 0, 22);
|
||||
//
|
||||
// if(index >= 0)
|
||||
// {
|
||||
// errors++;
|
||||
// }
|
||||
//
|
||||
// int corrected = message.getInt(0, 22);
|
||||
//
|
||||
// if(Integer.bitCount(original ^ corrected) > 3)
|
||||
// {
|
||||
// return 2;
|
||||
// }
|
||||
//
|
||||
// return 1;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// message.rotateLeft(0, 22);
|
||||
// syndrome = getSyndrome(message);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// index++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return 2;
|
||||
// }
|
||||
|
||||
private static long getSyndrome(BinaryMessage message)
|
||||
{
|
||||
long calculated = calculateChecksum(message);
|
||||
long checksum = message.getInt(12, 22);
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// Binary40_9_16 edac = new Binary40_9_16();
|
||||
|
||||
long mask = 0xFFFFFFFFFFl;
|
||||
long sync = 0x575D57F7FFl;
|
||||
long syncLeft = Long.rotateLeft(sync, 2) & mask;
|
||||
long syncRight = Long.rotateRight(sync, 2) & mask;
|
||||
System.out.println("Mask: " + Long.toBinaryString(sync));
|
||||
System.out.println("Left: " + Long.toBinaryString(syncLeft));
|
||||
System.out.println("Right: " + Long.toBinaryString(syncRight));
|
||||
|
||||
int distanceLeft = Long.bitCount(sync ^ syncLeft);
|
||||
int distanceRight = Long.bitCount( sync ^ syncRight);
|
||||
|
||||
System.out.println("Left: " + distanceLeft);
|
||||
System.out.println("Right: " + distanceRight);
|
||||
|
||||
System.out.println("Finished!");
|
||||
|
||||
|
||||
// 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 = Binary40_9_16.checkAndCorrect(bm);
|
||||
// System.out.println("M:" + bm.toHexString());
|
||||
//
|
||||
// System.out.println("A:" + a);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -216,6 +220,63 @@ public class CRCP25
|
||||
0x20000000l, 0x40000000l, 0x80000000l
|
||||
};
|
||||
|
||||
/**
|
||||
* CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 144-bit message and 12-bits of checksum
|
||||
* using a polynomial of 0x1897 and generated by:
|
||||
* <p>
|
||||
* long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
|
||||
*/
|
||||
private static int[] CRC_12_FACCH = new int[]{0x256, 0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369,
|
||||
0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5, 0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB,
|
||||
0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4, 0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB,
|
||||
0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0, 0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1,
|
||||
0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4, 0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F,
|
||||
0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565, 0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49,
|
||||
0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA, 0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4,
|
||||
0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284, 0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5,
|
||||
0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F, 0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917,
|
||||
0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23, 0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897};
|
||||
|
||||
/**
|
||||
* CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 168-bit message and 12-bits of checksum
|
||||
* using a polynomial of 0x1897 and generated by:
|
||||
* <p>
|
||||
* long[] checksums = CRCUtil.generate(168, 12, 0x1897l, 0x0l, false);
|
||||
*/
|
||||
private static int[] CRC_12_SACCH = new int[]{0x47B, 0xE76, 0x73B, 0xFD6, 0x7EB, 0xFBE, 0x7DF, 0xFA4, 0x7D2, 0x3E9,
|
||||
0xDBF, 0xA94, 0x54A, 0x2A5, 0xD19, 0xAC7, 0x928, 0x494, 0x24A, 0x125, 0xCD9, 0xA27, 0x958, 0x4AC, 0x256,
|
||||
0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369, 0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5,
|
||||
0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB, 0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4,
|
||||
0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB, 0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0,
|
||||
0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1, 0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4,
|
||||
0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F, 0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565,
|
||||
0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49, 0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA,
|
||||
0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4, 0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284,
|
||||
0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5, 0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F,
|
||||
0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917, 0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23,
|
||||
0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897,};
|
||||
|
||||
/**
|
||||
* CRC-16 checksums for P25 Phase 2 LCCH MAC PDU Contents. This is used for a 164-bit message and 16-bits of
|
||||
* checksum using a polynomial of 0x11021 and generated by:
|
||||
* <p>
|
||||
* long[] checksums = CRCUtil.generate(164, 16, 0x11021l, 0x0l, true);
|
||||
*/
|
||||
private static int[] CRC_16_LCCH = new int[]{0xCF76, 0x67BB, 0xBBCD, 0xD5F6, 0x6AFB, 0xBD6D, 0xD6A6, 0x6B53, 0xBDB9,
|
||||
0xD6CC, 0x6B66, 0x35B3, 0x92C9, 0xC174, 0x60BA, 0x305D, 0x903E, 0x481F, 0xAC1F, 0xDE1F, 0xE71F, 0xFB9F,
|
||||
0xF5DF, 0xF2FF, 0xF16F, 0xF0A7, 0xF043, 0xF031, 0xF008, 0x7804, 0x3C02, 0x1E01, 0x8710, 0x4388, 0x21C4,
|
||||
0x10E2, 0x871, 0x8C28, 0x4614, 0x230A, 0x1185, 0x80D2, 0x4069, 0xA824, 0x5412, 0x2A09, 0x9D14, 0x4E8A,
|
||||
0x2745, 0x9BB2, 0x4DD9, 0xAEFC, 0x577E, 0x2BBF, 0x9DCF, 0xC6F7, 0xEB6B, 0xFDA5, 0xF6C2, 0x7B61, 0xB5A0,
|
||||
0x5AD0, 0x2D68, 0x16B4, 0xB5A, 0x5AD, 0x8AC6, 0x4563, 0xAAA1, 0xDD40, 0x6EA0, 0x3750, 0x1BA8, 0xDD4, 0x6EA,
|
||||
0x375, 0x89AA, 0x44D5, 0xAA7A, 0x553D, 0xA28E, 0x5147, 0xA0B3, 0xD849, 0xE434, 0x721A, 0x390D, 0x9496,
|
||||
0x4A4B, 0xAD35, 0xDE8A, 0x6F45, 0xBFB2, 0x5FD9, 0xA7FC, 0x53FE, 0x29FF, 0x9CEF, 0xC667, 0xEB23, 0xFD81,
|
||||
0xF6D0, 0x7B68, 0x3DB4, 0x1EDA, 0xF6D, 0x8FA6, 0x47D3, 0xABF9, 0xDDEC, 0x6EF6, 0x377B, 0x93AD, 0xC1C6,
|
||||
0x60E3, 0xB861, 0xD420, 0x6A10, 0x3508, 0x1A84, 0xD42, 0x6A1, 0x8B40, 0x45A0, 0x22D0, 0x1168, 0x8B4, 0x45A,
|
||||
0x22D, 0x8906, 0x4483, 0xAA51, 0xDD38, 0x6E9C, 0x374E, 0x1BA7, 0x85C3, 0xCAF1, 0xED68, 0x76B4, 0x3B5A,
|
||||
0x1DAD, 0x86C6, 0x4363, 0xA9A1, 0xDCC0, 0x6E60, 0x3730, 0x1B98, 0xDCC, 0x6E6, 0x373, 0x89A9, 0xCCC4, 0x6662,
|
||||
0x3331, 0x9188, 0x48C4, 0x2462, 0x1231, 0x8108, 0x4084, 0x2042, 0x1021, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20,
|
||||
0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000};
|
||||
|
||||
/**
|
||||
* Performs error detection and single-bit error correction against the
|
||||
* data blocks of a PDU1 message.
|
||||
@@ -402,11 +463,8 @@ public class CRCP25
|
||||
}
|
||||
|
||||
int checksum = message.getInt(messageStart + 7, messageStart + 15);
|
||||
|
||||
int residual = calculated ^ checksum;
|
||||
|
||||
// mLog.debug( "CALC:" + calculated + " CHECK:" + checksum + " RESID:" + residual );
|
||||
|
||||
if(residual == 0 || residual == 0x1FF)
|
||||
{
|
||||
return CRC.PASSED;
|
||||
@@ -441,12 +499,80 @@ public class CRCP25
|
||||
return passes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for S-OEMI/FACCH and compares it to the
|
||||
* transmitted checksum to verify that the message is correct.
|
||||
* Note: this uses the pre-calculated checksums in the CRC_12_FACCH array.
|
||||
*
|
||||
* @param message to verify
|
||||
* @return true if the message passes the CRC-12 check.
|
||||
*/
|
||||
public static boolean crc12_FACCH(CorrectedBinaryMessage message)
|
||||
{
|
||||
int calculated = 0xFFF; //Initial fill of all ones.
|
||||
|
||||
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
|
||||
for(int i = message.nextSetBit(0); i >= 0 && i < 144; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CRC_12_FACCH[i];
|
||||
}
|
||||
|
||||
int checksum = message.getInt(144, 155); //12-bit transmitted checksum
|
||||
int residual = calculated ^ checksum;
|
||||
return residual == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for I-OEMI/SACCH and compares it to the
|
||||
* transmitted checksum to verify that the message is correct.
|
||||
* Note: this uses the pre-calculated checksums in the CRC_12_SACCH array.
|
||||
*
|
||||
* @param message to verify
|
||||
* @return true if the message passes the CRC-12 check.
|
||||
*/
|
||||
public static boolean crc12_SACCH(CorrectedBinaryMessage message)
|
||||
{
|
||||
int calculated = 0xFFF; //Initial fill of all ones.
|
||||
|
||||
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
|
||||
for(int i = message.nextSetBit(0); i >= 0 && i < 168; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CRC_12_SACCH[i];
|
||||
}
|
||||
|
||||
int checksum = message.getInt(168, 179); //12-bit transmitted checksum
|
||||
int residual = calculated ^ checksum;
|
||||
return residual == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the CRC-16 checksum for the P25 Phase 2 MAC PDU Contents for I-OECI/LCCH and compares it to the
|
||||
* transmitted checksum to verify that the message is correct.
|
||||
* Note: this uses the pre-calculated checksums in the CRC_16_LCCH array.
|
||||
*
|
||||
* @param message to verify
|
||||
* @return true if the message passes the CRC-16 check.
|
||||
*/
|
||||
public static boolean crc16_LCCH(CorrectedBinaryMessage message)
|
||||
{
|
||||
int calculated = 0xFFFF; //Initial fill of all ones.
|
||||
|
||||
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
|
||||
for(int i = message.nextSetBit(0); i >= 0 && i < 164; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CRC_16_LCCH[i];
|
||||
}
|
||||
|
||||
int checksum = message.getInt(164, 179); //16-bit transmitted checksum
|
||||
int residual = calculated ^ checksum;
|
||||
return residual == 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the value of the message checksum as a long
|
||||
*/
|
||||
public static long getLongChecksum(BinaryMessage message,
|
||||
int crcStart, int crcLength)
|
||||
public static long getLongChecksum(BinaryMessage message, int crcStart, int crcLength)
|
||||
{
|
||||
return message.getLong(crcStart, crcStart + crcLength - 1);
|
||||
}
|
||||
@@ -454,8 +580,7 @@ public class CRCP25
|
||||
/**
|
||||
* Calculates the value of the message checksum as an integer
|
||||
*/
|
||||
public static int getIntChecksum(BinaryMessage message,
|
||||
int crcStart, int crcLength)
|
||||
public static int getIntChecksum(BinaryMessage message, int crcStart, int crcLength)
|
||||
{
|
||||
return message.getInt(crcStart, crcStart + crcLength - 1);
|
||||
}
|
||||
@@ -494,16 +619,19 @@ public class CRCP25
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String raw = "000000001000001100000001010001111011000100001010010001111100000000000101000000000000000001000000000000110000000000000001101010101010101010101010";
|
||||
long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
|
||||
System.out.println("Checksums:" + Arrays.toString(checksums));
|
||||
|
||||
BinaryMessage message = BinaryMessage.load(raw);
|
||||
String hex12 = "7C0000000000000000000000000000000000CA7";
|
||||
|
||||
mLog.debug("MSG:" + message.toString());
|
||||
|
||||
CRC results = checkCRC9(message, 0);
|
||||
|
||||
mLog.debug("COR:" + message.toString());
|
||||
|
||||
mLog.debug("Results: " + results.getDisplayText());
|
||||
// BinaryMessage message = BinaryMessage.load(raw);
|
||||
//
|
||||
// mLog.debug("MSG:" + message.toString());
|
||||
//
|
||||
// CRC results = checkCRC9(message, 0);
|
||||
//
|
||||
// mLog.debug("COR:" + message.toString());
|
||||
//
|
||||
// mLog.debug("Results: " + results.getDisplayText());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,6 +19,7 @@
|
||||
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;
|
||||
|
||||
@@ -32,8 +33,7 @@ public class CRCUtil
|
||||
long initialFill,
|
||||
boolean includeCRCBitErrors)
|
||||
{
|
||||
long[] crcTable = new long[messageSize +
|
||||
(includeCRCBitErrors ? crcSize : 0)];
|
||||
long[] crcTable = new long[messageSize + (includeCRCBitErrors ? crcSize : 0)];
|
||||
|
||||
int[] checksumIndexes = new int[crcSize];
|
||||
|
||||
@@ -132,20 +132,12 @@ public class CRCUtil
|
||||
* @return message with all message bits zeroed out, and the remainder
|
||||
* placed in the crc field which starts at index messageLength
|
||||
*/
|
||||
public static BinaryMessage decode(BinaryMessage message,
|
||||
int messageStart,
|
||||
int messageSize,
|
||||
long polynomial,
|
||||
int crcSize)
|
||||
public static BinaryMessage decode(BinaryMessage message, int messageStart, int messageSize, long polynomial, int crcSize)
|
||||
{
|
||||
for(int i = message.nextSetBit(messageStart);
|
||||
i >= messageStart && i < messageSize;
|
||||
i = message.nextSetBit(i + 1))
|
||||
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < messageSize; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
|
||||
|
||||
polySet.load(i, crcSize + 1, polynomial);
|
||||
|
||||
message.xor(polySet);
|
||||
System.out.println(message.toString());
|
||||
}
|
||||
@@ -226,8 +218,11 @@ public class CRCUtil
|
||||
{
|
||||
mLog.debug("Starting");
|
||||
|
||||
long poly = 0x13l;
|
||||
long[] checksums = generate(32, 4, poly, 0, true);
|
||||
int messageSize = 164;
|
||||
int crcSize = 16;
|
||||
long poly = 0x11021l;
|
||||
long initialFill = 0x0l;
|
||||
long[] checksums = CRCUtil.generate(messageSize, crcSize, poly, initialFill, true);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("private static int[] CHECKSUMS = new int[]{");
|
||||
@@ -238,7 +233,29 @@ public class CRCUtil
|
||||
}
|
||||
|
||||
sb.append("};");
|
||||
|
||||
System.out.println("Checksums:\n" + sb);
|
||||
|
||||
String hex = "7CFC0039420170452A4F9970000000000000000000D05";
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
|
||||
|
||||
|
||||
int calculated = 0xFFF; //Initial fill of all ones
|
||||
|
||||
int messageStart = 0;
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = message.nextSetBit(messageStart);
|
||||
i >= messageStart && i < messageSize;
|
||||
i = message.nextSetBit(i + 1))
|
||||
{
|
||||
System.out.println("Bit [" + i + "] is set");
|
||||
calculated ^= checksums[i];
|
||||
}
|
||||
|
||||
int checksum = message.getInt(messageSize, messageSize + crcSize - 1);
|
||||
int residual = calculated ^ checksum;
|
||||
|
||||
mLog.debug("CALC:" + Integer.toHexString(calculated).toUpperCase() +
|
||||
" CHECK:" + Integer.toHexString(checksum).toUpperCase() +
|
||||
" RESIDUAL:" + Integer.toHexString(residual).toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -35,7 +35,7 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.viewer.RecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.MessageRecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
@@ -84,7 +84,7 @@ public class JavaFxWindowManager extends Application
|
||||
private TunerManager mTunerManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private UserPreferencesEditor mUserPreferencesEditor;
|
||||
private RecordingViewer mRecordingViewer;
|
||||
private MessageRecordingViewer mMessageRecordingViewer;
|
||||
|
||||
private Stage mChannelMapStage;
|
||||
private Stage mIconManagerStage;
|
||||
@@ -216,14 +216,14 @@ public class JavaFxWindowManager extends Application
|
||||
return mRecordingViewerStage;
|
||||
}
|
||||
|
||||
public RecordingViewer getRecordingViewer()
|
||||
public MessageRecordingViewer getRecordingViewer()
|
||||
{
|
||||
if(mRecordingViewer == null)
|
||||
if(mMessageRecordingViewer == null)
|
||||
{
|
||||
mRecordingViewer = new RecordingViewer();
|
||||
mMessageRecordingViewer = new MessageRecordingViewer();
|
||||
}
|
||||
|
||||
return mRecordingViewer;
|
||||
return mMessageRecordingViewer;
|
||||
}
|
||||
|
||||
public Stage getIconManagerStage()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -43,6 +43,7 @@ import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.log.ApplicationLog;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.DiagnosticMonitor;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -54,6 +55,7 @@ import io.github.dsheirer.source.tuner.Tuner;
|
||||
import io.github.dsheirer.source.tuner.TunerEvent;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayLibraryHelper;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||
@@ -125,6 +127,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
private ControllerPanel mControllerPanel;
|
||||
private DiagnosticMonitor mDiagnosticMonitor;
|
||||
private IconModel mIconModel = new IconModel();
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
@@ -150,6 +153,14 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||
mApplicationLog.start();
|
||||
|
||||
//Note: invoke this early in the application lifecycle, before the TunerManager causes the sdrplay classes
|
||||
//to be loaded since the jextract auto-generated code attempts to load the library by name and that can fail
|
||||
//when the library was not installed into a normal/default location, particularly on windows OS systems.
|
||||
if(SDRPlayLibraryHelper.LOADED)
|
||||
{
|
||||
mLog.info("SDRPlay API native library preemptively loaded");
|
||||
}
|
||||
|
||||
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
@@ -187,7 +198,13 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, mIconModel);
|
||||
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
boolean headless = GraphicsEnvironment.isHeadless();
|
||||
|
||||
mDiagnosticMonitor = new DiagnosticMonitor(mUserPreferences, mPlaylistManager.getChannelProcessingManager(),
|
||||
mTunerManager, headless);
|
||||
mDiagnosticMonitor.start();
|
||||
|
||||
if(!headless)
|
||||
{
|
||||
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences, mTunerManager, mPlaylistManager);
|
||||
}
|
||||
@@ -214,7 +231,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
|
||||
|
||||
MapService mapService = new MapService(mIconModel);
|
||||
MapService mapService = new MapService(aliasModel, mIconModel);
|
||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||
|
||||
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||
@@ -411,6 +428,46 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
JMenuItem processingStatusReportMenuItem = new JMenuItem("Processing Diagnostic Report");
|
||||
processingStatusReportMenuItem.addActionListener(e -> {
|
||||
try
|
||||
{
|
||||
Path path = mDiagnosticMonitor.generateProcessingDiagnosticReport("User initiated diagnostic report");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui, "Report created: " +
|
||||
path.toString(), "Processing Status Report Created", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error creating processing status report file", ioe);
|
||||
JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " +
|
||||
"see application log for details.", "Processing Status Report Failed", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
JMenuItem threadDumpReportMenuItem = new JMenuItem("Thread Dump Report");
|
||||
threadDumpReportMenuItem.addActionListener(e -> {
|
||||
try
|
||||
{
|
||||
Path path = mDiagnosticMonitor.generateThreadDumpReport();
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui, "Report created: " +
|
||||
path.toString(), "Thread Dump Report Created", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error creating thread dump report file", ioe);
|
||||
JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " +
|
||||
"see application log for details.", "Thread Dump Report Failed", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
JMenu diagnosticMenu = new JMenu(("Reports"));
|
||||
diagnosticMenu.add(processingStatusReportMenuItem);
|
||||
diagnosticMenu.add(threadDumpReportMenuItem);
|
||||
fileMenu.add(diagnosticMenu);
|
||||
fileMenu.add(new JSeparator(JSeparator.HORIZONTAL));
|
||||
|
||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||
exitMenu.addActionListener(event -> {
|
||||
processShutdown();
|
||||
@@ -578,6 +635,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
private void processShutdown()
|
||||
{
|
||||
mLog.info("Application shutdown started ...");
|
||||
mDiagnosticMonitor.stop();
|
||||
mUserPreferences.getSwingPreference().setLocation(WINDOW_FRAME_IDENTIFIER, mMainGui.getLocation());
|
||||
mUserPreferences.getSwingPreference().setDimension(WINDOW_FRAME_IDENTIFIER, mMainGui.getSize());
|
||||
mUserPreferences.getSwingPreference().setMaximized(WINDOW_FRAME_IDENTIFIER,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -35,6 +35,13 @@ import io.github.dsheirer.spectrum.ComplexDftProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -47,13 +54,6 @@ import javax.swing.JToggleButton;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
@@ -262,7 +262,7 @@ public class ChannelizerViewer extends JFrame
|
||||
for(int x = 0; x < mChannelCount; x++)
|
||||
{
|
||||
TunerChannel tunerChannel = new TunerChannel(100000000, 12500);
|
||||
TunerChannelSource source = mTestTuner.getChannelSourceManager().getSource(tunerChannel, null);
|
||||
TunerChannelSource source = mTestTuner.getChannelSourceManager().getSource(tunerChannel, null, "test");
|
||||
DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x);
|
||||
channelPanel.setDFTSize(mChannelPanelDFTSize);
|
||||
|
||||
@@ -362,7 +362,7 @@ public class ChannelizerViewer extends JFrame
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth);
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel, null);
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel, null, "test");
|
||||
|
||||
if(mSource != null)
|
||||
{
|
||||
@@ -535,7 +535,7 @@ public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
if(sourceCount < maxSourceCount)
|
||||
{
|
||||
TunerChannelSource source = tuner.getChannelSourceManager().getSource(tunerChannel, null);
|
||||
TunerChannelSource source = tuner.getChannelSourceManager().getSource(tunerChannel, null, "test");
|
||||
|
||||
if(source != null)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -379,7 +379,8 @@ public class ChannelizerViewer2 extends JFrame
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth);
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel, new ChannelSpecification(50000, 12500, 6000, 7000));
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel,
|
||||
new ChannelSpecification(50000, 12500, 6000, 7000), "test");
|
||||
|
||||
if(mSource != null)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -36,6 +36,13 @@ import io.github.dsheirer.spectrum.ComplexDftProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -48,13 +55,6 @@ import javax.swing.JToggleButton;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class HeterodyneChannelizerViewer extends JFrame
|
||||
{
|
||||
@@ -261,7 +261,8 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
for(int x = 0; x < mChannelCount; x++)
|
||||
{
|
||||
TunerChannel tunerChannel = new TunerChannel(100000000, 12500);
|
||||
TunerChannelSource source = mTestTuner.getChannelSourceManager().getSource(tunerChannel, channelSpecification);
|
||||
TunerChannelSource source = mTestTuner.getChannelSourceManager().getSource(tunerChannel,
|
||||
channelSpecification, "test");
|
||||
DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x);
|
||||
channelPanel.setDFTSize(mChannelPanelDFTSize);
|
||||
|
||||
@@ -362,7 +363,7 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth);
|
||||
ChannelSpecification channelSpecification = new ChannelSpecification(25000.0, 12500, 6000.0, 6250.0);
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel, channelSpecification);
|
||||
mSource = mTestTuner.getChannelSourceManager().getSource(tunerChannel, channelSpecification, "test");
|
||||
|
||||
if(mSource != null)
|
||||
{
|
||||
@@ -533,7 +534,8 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
{
|
||||
if(sourceCount < maxSourceCount)
|
||||
{
|
||||
TunerChannelSource source = tuner.getChannelSourceManager().getSource(tunerChannel, null);
|
||||
TunerChannelSource source = tuner.getChannelSourceManager().getSource(tunerChannel,
|
||||
null, "test");
|
||||
|
||||
if(source != null)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public class DbPowerMeter extends JComponent
|
||||
*/
|
||||
public DbPowerMeter()
|
||||
{
|
||||
setPreferredSize(new Dimension(80, 100));
|
||||
setPreferredSize(new Dimension(90, 100));
|
||||
setBorder(BorderFactory.createTitledBorder("Power (dB)"));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.control;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Document;
|
||||
import javax.swing.text.DocumentFilter;
|
||||
import javax.swing.text.PlainDocument;
|
||||
|
||||
/**
|
||||
* Swing text field control for entering frequency (MHz) values.
|
||||
*/
|
||||
public class FrequencyTextField extends JTextField
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FrequencyTextField.class);
|
||||
//Value range: 0.000001 to 9999.999999
|
||||
private static final String REGEX = "^[0-9]{0,4}[.]?[0-9]{0,6}$";
|
||||
//This lets users start typing a really small number like 1 Hertz ... 0.00000
|
||||
private static final String ZEROS_REGEX = "^0?([.]0{0,5})?$";
|
||||
private double mMinimum;
|
||||
private double mMaximum;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param minimum allowable frequency value in Hertz
|
||||
* @param maximum allowable frequency value in Hertz
|
||||
* @param current frequency value in Hertz
|
||||
*/
|
||||
public FrequencyTextField(long minimum, long maximum, long current)
|
||||
{
|
||||
super(8);
|
||||
mMinimum = minimum / 1E6d;
|
||||
mMaximum = maximum / 1E6d;
|
||||
|
||||
PlainDocument document = (PlainDocument)this.getDocument();
|
||||
document.setDocumentFilter(new FrequencyFilter());
|
||||
setFrequency(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current frequency value
|
||||
* @return frequency in Hertz
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
String value = getText();
|
||||
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (long)(Double.parseDouble(getText()) * 1E6d);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.error("Unable to parse frequency value from text [" + value + "] " + e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current frequency value
|
||||
* @param frequency in Hertz
|
||||
*/
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
double frequencyMHz = frequency / 1E6d;
|
||||
|
||||
if(isValid(String.valueOf(frequencyMHz)))
|
||||
{
|
||||
setText(String.valueOf(frequencyMHz));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.warn("Can't set frequency [" + frequency + "Hz / " + frequencyMHz + "MHz] with current minimum [" + mMinimum + "MHz] and maximum [" + mMaximum + "MHz] limits");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the value is a valid double value that is between the minimum and maximum extents.
|
||||
* @param value to test
|
||||
* @return true if it is valid.
|
||||
*/
|
||||
private boolean isValid(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty() || value.matches(ZEROS_REGEX))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(value.matches(REGEX))
|
||||
{
|
||||
try
|
||||
{
|
||||
double frequency = Double.parseDouble(value);
|
||||
return mMinimum <= frequency && frequency <= mMaximum;
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input filter for user entered values.
|
||||
*/
|
||||
class FrequencyFilter extends DocumentFilter
|
||||
{
|
||||
@Override
|
||||
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException
|
||||
{
|
||||
Document doc = fb.getDocument();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(doc.getText(0, doc.getLength()));
|
||||
sb.insert(offset, string);
|
||||
|
||||
if(isValid(sb.toString()))
|
||||
{
|
||||
super.insertString(fb, offset, string, attr);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException
|
||||
{
|
||||
Document doc = fb.getDocument();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(doc.getText(0, doc.getLength()));
|
||||
sb.replace(offset, offset + length, text);
|
||||
|
||||
if(isValid(sb.toString()))
|
||||
{
|
||||
super.replace(fb, offset, length, text, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException
|
||||
{
|
||||
Document doc = fb.getDocument();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(doc.getText(0, doc.getLength()));
|
||||
sb.delete(offset, offset + length);
|
||||
|
||||
if(isValid(sb.toString()))
|
||||
{
|
||||
super.remove(fb, offset, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
JFrame frame = new JFrame("Frequency Control Test");
|
||||
frame.setSize(300, 200);
|
||||
FrequencyTextField ftf = new FrequencyTextField(1, 9_999_999_999l, 101_100_000);
|
||||
frame.setLayout(new MigLayout());
|
||||
frame.add(ftf);
|
||||
|
||||
EventQueue.invokeLater(() -> frame.setVisible(true));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,20 +19,18 @@
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasItemEditor;
|
||||
import java.util.function.UnaryOperator;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Text formatter for integer values that constrains values to specified minimum and maximum valid values.
|
||||
*/
|
||||
public class IntegerFormatter extends TextFormatter<Integer>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AliasItemEditor.class);
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IntegerFormatter.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -434,8 +434,7 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
||||
|
||||
if(se instanceof InvalidFrequencyException ife)
|
||||
{
|
||||
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
|
||||
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
|
||||
JOptionPane.showMessageDialog(this, ife.getMessage() + " for this tuner.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
src/main/java/io/github/dsheirer/gui/control/LongFormatter.java
Normal file
111
src/main/java/io/github/dsheirer/gui/control/LongFormatter.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.control;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.converter.LongStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Text formatter for long values that constrains values to specified minimum and maximum valid values.
|
||||
*/
|
||||
public class LongFormatter extends TextFormatter<Long>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(LongFormatter.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum allowed value
|
||||
* @param maximum allowed value
|
||||
*/
|
||||
public LongFormatter(int minimum, int maximum)
|
||||
{
|
||||
super(new LongStringConverter(), null, new LongFilter(minimum, maximum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted text change filter that only allows hexadecimal characters where the converted decimal value
|
||||
* is also constrained within minimum and maximum valid values.
|
||||
*/
|
||||
public static class LongFilter implements UnaryOperator<Change>
|
||||
{
|
||||
private String DECIMAL_REGEX = "\\-?[0-9].*";
|
||||
private int mMinimum;
|
||||
private int mMaximum;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum value
|
||||
* @param maximum value
|
||||
*/
|
||||
public LongFilter(int minimum, int maximum)
|
||||
{
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the value argument is parsable as an integer, or is empty or null.
|
||||
*/
|
||||
private boolean isValid(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
long parsed = Long.parseLong(value);
|
||||
return mMinimum <= parsed && parsed <= mMaximum;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change)
|
||||
{
|
||||
//Only validate if the user added text to the control. Otherwise, allow it to go through
|
||||
if(change.getText() != null)
|
||||
{
|
||||
String updatedText = change.getControlNewText();
|
||||
|
||||
if(updatedText == null || updatedText.isEmpty())
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
if(!updatedText.matches(DECIMAL_REGEX) || !isValid(updatedText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -647,11 +647,13 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
public NewP25P2ChannelMenu()
|
||||
{
|
||||
setText(DecoderType.P25_PHASE2.getDisplayString());
|
||||
MenuItem trunked = new MenuItem("Trunked System");
|
||||
trunked.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
|
||||
MenuItem channel = new MenuItem("Individual Channel");
|
||||
MenuItem trunkedP1 = new MenuItem("Trunked System - FDMA Phase 1 Control Channel");
|
||||
trunkedP1.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
|
||||
MenuItem trunkedP2 = new MenuItem("Trunked System - TDMA Phase 2 Control Channel");
|
||||
trunkedP2.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
|
||||
MenuItem channel = new MenuItem("Individual Phase 2 Channel");
|
||||
channel.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
|
||||
getItems().addAll(trunked, channel);
|
||||
getItems().addAll(trunkedP1, trunkedP2, channel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -113,7 +113,7 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
||||
if(mDecoderPane == null)
|
||||
{
|
||||
mDecoderPane = new TitledPane();
|
||||
mDecoderPane.setText("Decoder: P25 Phase 1 (or P25 Phase 2 Control Channel)");
|
||||
mDecoderPane.setText("Decoder: P25 Phase 1 (also used for P25 Phase 2 system with FDMA control channels)");
|
||||
mDecoderPane.setExpanded(true);
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
@@ -355,9 +355,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
DecodeConfigP25Phase1 config;
|
||||
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1)
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1)
|
||||
{
|
||||
config = (DecodeConfigP25Phase1)getItem().getDecodeConfiguration();
|
||||
config = p1;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -42,9 +42,13 @@ import java.util.List;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -64,6 +68,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
private IntegerTextField mWacnTextField;
|
||||
private IntegerTextField mSystemTextField;
|
||||
private IntegerTextField mNacTextField;
|
||||
private ToggleSwitch mIgnoreDataCallsButton;
|
||||
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -102,7 +108,7 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
if(mDecoderPane == null)
|
||||
{
|
||||
mDecoderPane = new TitledPane();
|
||||
mDecoderPane.setText("Decoder: P25 Phase 2");
|
||||
mDecoderPane.setText("Decoder: P25 Phase 2 for TDMA control or TDMA traffic channels.");
|
||||
mDecoderPane.setExpanded(true);
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
@@ -112,6 +118,22 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label poolSizeLabel = new Label("Max Traffic Channels");
|
||||
GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(poolSizeLabel, 0, row);
|
||||
gridPane.getChildren().add(poolSizeLabel);
|
||||
|
||||
GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row);
|
||||
gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
|
||||
|
||||
GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row);
|
||||
gridPane.getChildren().add(getIgnoreDataCallsButton());
|
||||
|
||||
Label directionLabel = new Label("Ignore Data Calls");
|
||||
GridPane.setHalignment(directionLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(directionLabel, 3, row);
|
||||
gridPane.getChildren().add(directionLabel);
|
||||
|
||||
Label wacnLabel = new Label("WACN");
|
||||
GridPane.setHalignment(wacnLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(wacnLabel, 0, ++row);
|
||||
@@ -122,20 +144,27 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
GridPane.setConstraints(systemLabel, 2, row);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemTextField(), 1, row);
|
||||
GridPane.setConstraints(getSystemTextField(), 3, row);
|
||||
gridPane.getChildren().add(getSystemTextField());
|
||||
|
||||
Label nacLabel = new Label("NAC");
|
||||
GridPane.setHalignment(nacLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(nacLabel, 0, ++row);
|
||||
GridPane.setConstraints(nacLabel, 4, row);
|
||||
gridPane.getChildren().add(nacLabel);
|
||||
|
||||
GridPane.setConstraints(getNacTextField(), 1, row);
|
||||
GridPane.setConstraints(getNacTextField(), 5, row);
|
||||
gridPane.getChildren().add(getNacTextField());
|
||||
|
||||
Label noteLabel = new Label("Note: WACN/System/NAC values are auto-detected (ie not required) from " +
|
||||
"the control channel and are only required when decoding individual traffic channels");
|
||||
GridPane.setHalignment(noteLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(noteLabel, 1, ++row, 6, 1);
|
||||
gridPane.getChildren().add(noteLabel);
|
||||
|
||||
|
||||
mDecoderPane.setContent(gridPane);
|
||||
}
|
||||
|
||||
@@ -203,6 +232,37 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
return mEventLogConfigurationEditor;
|
||||
}
|
||||
|
||||
private ToggleSwitch getIgnoreDataCallsButton()
|
||||
{
|
||||
if(mIgnoreDataCallsButton == null)
|
||||
{
|
||||
mIgnoreDataCallsButton = new ToggleSwitch();
|
||||
mIgnoreDataCallsButton.setDisable(true);
|
||||
mIgnoreDataCallsButton.selectedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mIgnoreDataCallsButton;
|
||||
}
|
||||
|
||||
private Spinner<Integer> getTrafficChannelPoolSizeSpinner()
|
||||
{
|
||||
if(mTrafficChannelPoolSizeSpinner == null)
|
||||
{
|
||||
mTrafficChannelPoolSizeSpinner = new Spinner();
|
||||
mTrafficChannelPoolSizeSpinner.setDisable(true);
|
||||
mTrafficChannelPoolSizeSpinner.setTooltip(
|
||||
new Tooltip("Maximum number of traffic channels that can be created by the decoder"));
|
||||
mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
|
||||
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50);
|
||||
mTrafficChannelPoolSizeSpinner.setValueFactory(svf);
|
||||
mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mTrafficChannelPoolSizeSpinner;
|
||||
}
|
||||
|
||||
private IntegerTextField getWacnTextField()
|
||||
{
|
||||
if(mWacnTextField == null)
|
||||
@@ -247,6 +307,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
types.add(RecorderType.BASEBAND);
|
||||
types.add(RecorderType.DEMODULATED_BIT_STREAM);
|
||||
types.add(RecorderType.MBE_CALL_SEQUENCE);
|
||||
types.add(RecorderType.TRAFFIC_BASEBAND);
|
||||
types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM);
|
||||
types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE);
|
||||
mRecordConfigurationEditor = new RecordConfigurationEditor(types);
|
||||
mRecordConfigurationEditor.setDisable(true);
|
||||
mRecordConfigurationEditor.modifiedProperty()
|
||||
@@ -259,9 +322,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
@Override
|
||||
protected void setDecoderConfiguration(DecodeConfiguration config)
|
||||
{
|
||||
if(config instanceof DecodeConfigP25Phase2)
|
||||
if(config instanceof DecodeConfigP25Phase2 decodeConfig)
|
||||
{
|
||||
DecodeConfigP25Phase2 decodeConfig = (DecodeConfigP25Phase2)config;
|
||||
getWacnTextField().setDisable(false);
|
||||
getSystemTextField().setDisable(false);
|
||||
getNacTextField().setDisable(false);
|
||||
@@ -280,6 +342,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
getSystemTextField().set(0);
|
||||
getNacTextField().set(0);
|
||||
}
|
||||
|
||||
getIgnoreDataCallsButton().setDisable(false);
|
||||
getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
|
||||
getTrafficChannelPoolSizeSpinner().setDisable(false);
|
||||
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -289,6 +356,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
getWacnTextField().setDisable(true);
|
||||
getSystemTextField().setDisable(true);
|
||||
getNacTextField().setDisable(true);
|
||||
getIgnoreDataCallsButton().setDisable(true);
|
||||
getTrafficChannelPoolSizeSpinner().setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,20 +366,23 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
DecodeConfigP25Phase2 config;
|
||||
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2)
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2 p2)
|
||||
{
|
||||
config = (DecodeConfigP25Phase2)getItem().getDecodeConfiguration();
|
||||
config.setAutoDetectScrambleParameters(false);
|
||||
int wacn = getWacnTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNacTextField().get();
|
||||
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
|
||||
config = p2;
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new DecodeConfigP25Phase2();
|
||||
}
|
||||
|
||||
config.setAutoDetectScrambleParameters(false);
|
||||
int wacn = getWacnTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNacTextField().get();
|
||||
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
|
||||
config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
|
||||
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
|
||||
|
||||
getItem().setDecodeConfiguration(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -25,13 +25,15 @@ import io.github.dsheirer.rrapi.type.Site;
|
||||
/**
|
||||
* Wrapper class to join a Site and a corresponding County Info
|
||||
*/
|
||||
public class EnrichedSite
|
||||
public class EnrichedSite implements Comparable<EnrichedSite>
|
||||
{
|
||||
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
|
||||
private Site mSite;
|
||||
private CountyInfo mCountyInfo;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param site object
|
||||
* @param countyInfo optional
|
||||
*/
|
||||
@@ -41,8 +43,16 @@ public class EnrichedSite
|
||||
mCountyInfo = countyInfo;
|
||||
}
|
||||
|
||||
public static String format(int value)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(value).append(" (").append(Integer.toHexString(value).toUpperCase()).append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Site instance
|
||||
*
|
||||
* @return site or null
|
||||
*/
|
||||
public Site getSite()
|
||||
@@ -52,6 +62,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* Sets the site instance
|
||||
*
|
||||
* @param site or null
|
||||
*/
|
||||
public void setSite(Site site)
|
||||
@@ -61,6 +72,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* County information
|
||||
*
|
||||
* @return county info or null
|
||||
*/
|
||||
public CountyInfo getCountyInfo()
|
||||
@@ -70,6 +82,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* Sets the county info
|
||||
*
|
||||
* @param countyInfo or null
|
||||
*/
|
||||
public void setCountyInfo(CountyInfo countyInfo)
|
||||
@@ -78,13 +91,26 @@ public class EnrichedSite
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional site number
|
||||
* Formatted system identity
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Integer getSiteNumber()
|
||||
public String getSystemFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return mSite.getSiteNumber();
|
||||
//System number is stored in the zone number field.
|
||||
return format(mSite.getZoneNumber());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSiteFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return format(mSite.getSiteNumber());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -93,11 +119,11 @@ public class EnrichedSite
|
||||
/**
|
||||
* Optional site RFSS value
|
||||
*/
|
||||
public Integer getRfss()
|
||||
public String getRfssFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return mSite.getRfss();
|
||||
return format(mSite.getRfss());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,9 +149,28 @@ public class EnrichedSite
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return mSite.getDescription();
|
||||
return mSite.getDescription() + (mSite.getTdmaControlChannel() > 0 ? " (TDMA CC)" : "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String system = getSystemFormatted();
|
||||
sb.append(system == null ? "-" : system).append(" ");
|
||||
String rfss = getRfssFormatted();
|
||||
sb.append(rfss == null ? "-" : rfss).append(" ");
|
||||
String site = getSiteFormatted();
|
||||
sb.append(site == null ? "-" : site);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(EnrichedSite o)
|
||||
{
|
||||
return this.toString().compareTo(o.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -449,6 +449,16 @@ public class RadioReferenceDecoder
|
||||
* @return decoder type or null.
|
||||
*/
|
||||
public DecoderType getDecoderType(System system)
|
||||
{
|
||||
return getDecoderType(system, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoder type for the specified system, if supported.
|
||||
* @param system requiring a decoder type
|
||||
* @return decoder type or null.
|
||||
*/
|
||||
public DecoderType getDecoderType(System system, Site site)
|
||||
{
|
||||
Type type = getType(system);
|
||||
Flavor flavor = getFlavor(system);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Flavor;
|
||||
import io.github.dsheirer.rrapi.type.RadioNetwork;
|
||||
import io.github.dsheirer.rrapi.type.Site;
|
||||
import io.github.dsheirer.rrapi.type.SiteFrequency;
|
||||
@@ -42,6 +43,13 @@ import io.github.dsheirer.rrapi.type.System;
|
||||
import io.github.dsheirer.rrapi.type.SystemInformation;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -74,14 +82,6 @@ import org.controlsfx.control.SegmentedButton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class SiteEditor extends GridPane
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(SiteEditor.class);
|
||||
@@ -90,6 +90,8 @@ public class SiteEditor extends GridPane
|
||||
private static final String PRIMARY_CONTROL_CHANNEL = "d";
|
||||
private static final String TOGGLE_BUTTON_CONTROL = "Control";
|
||||
private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice";
|
||||
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
|
||||
private static final String PHASE_2_FLAVOR = "Phase II";
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@@ -115,6 +117,10 @@ public class SiteEditor extends GridPane
|
||||
private SystemInformation mCurrentSystemInformation;
|
||||
private ComboBox mAliasListNameComboBox;
|
||||
private Button mNewAliasListButton;
|
||||
private Label mP25ControlLabel;
|
||||
private SegmentedButton mP25ControlSegmentedButton;
|
||||
private ToggleButton mFdmaControlToggleButton;
|
||||
private ToggleButton mTdmaControlToggleButton;
|
||||
|
||||
public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManager)
|
||||
{
|
||||
@@ -163,6 +169,13 @@ public class SiteEditor extends GridPane
|
||||
GridPane.setConstraints(getConfigurationsSegmentedButton(), 2, row);
|
||||
getChildren().add(getConfigurationsSegmentedButton());
|
||||
|
||||
GridPane.setConstraints(getP25ControlLabel(), 1, ++row);
|
||||
GridPane.setHalignment(getP25ControlLabel(), HPos.RIGHT);
|
||||
getChildren().add(getP25ControlLabel());
|
||||
|
||||
GridPane.setConstraints(getP25ControlSegmentedButton(), 2, row);
|
||||
getChildren().add(getP25ControlSegmentedButton());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setConstraints(systemLabel, 1, ++row);
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
@@ -206,7 +219,7 @@ public class SiteEditor extends GridPane
|
||||
createBox.setAlignment(Pos.CENTER_LEFT);
|
||||
createBox.setSpacing(10);
|
||||
createBox.getChildren().addAll(getCreateChannelConfigurationButton(), getGoToChannelEditorCheckBox());
|
||||
GridPane.setConstraints(createBox, 2, ++row + 1);
|
||||
GridPane.setConstraints(createBox, 2, ++row);
|
||||
getChildren().addAll(createBox);
|
||||
|
||||
//Note: the following label node is added to the same location as the buttons and visibility is toggled
|
||||
@@ -215,6 +228,13 @@ public class SiteEditor extends GridPane
|
||||
getChildren().add(getProtocolNotSupportedLabel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel decode configuration for the specified site.
|
||||
* @param decoderType to create
|
||||
* @param site information and frequencies
|
||||
* @param systemInformation with frequency mapping
|
||||
* @return decode configuration
|
||||
*/
|
||||
private DecodeConfiguration getDecodeConfiguration(DecoderType decoderType, Site site, SystemInformation systemInformation)
|
||||
{
|
||||
if(decoderType == null)
|
||||
@@ -298,7 +318,13 @@ public class SiteEditor extends GridPane
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the user selected site info into this editor.
|
||||
* @param site to load
|
||||
* @param system for the site
|
||||
* @param systemInformation for the site
|
||||
* @param decoder to lookup supplemental information
|
||||
*/
|
||||
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
|
||||
{
|
||||
mCurrentSite = site;
|
||||
@@ -346,6 +372,30 @@ public class SiteEditor extends GridPane
|
||||
}
|
||||
else
|
||||
{
|
||||
Flavor flavor = decoder.getFlavor(system);
|
||||
|
||||
if(flavor != null && flavor.getName() != null && flavor.getName().equals(PHASE_2_FLAVOR))
|
||||
{
|
||||
getP25ControlLabel().setVisible(true);
|
||||
getTdmaControlToggleButton().setVisible(true);
|
||||
getFdmaControlToggleButton().setVisible(true);
|
||||
|
||||
if(site.getSite().getTdmaControlChannel() > 0) //Value is 0 for FDMA or 1 for TDMA
|
||||
{
|
||||
getTdmaControlToggleButton().setSelected(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
getFdmaControlToggleButton().setSelected(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
getP25ControlLabel().setVisible(false);
|
||||
getTdmaControlToggleButton().setVisible(false);
|
||||
getFdmaControlToggleButton().setVisible(false);
|
||||
}
|
||||
|
||||
getCreateChannelConfigurationButton().setVisible(true);
|
||||
getGoToChannelEditorCheckBox().setVisible(true);
|
||||
getSystemTextField().setText(system.getName());
|
||||
@@ -514,10 +564,13 @@ public class SiteEditor extends GridPane
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Change a phase 2 system to use the phase 1 control channel
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
|
||||
@@ -587,10 +640,13 @@ public class SiteEditor extends GridPane
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Change a phase 2 system to use the phase 1 control channel
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
|
||||
@@ -685,6 +741,16 @@ public class SiteEditor extends GridPane
|
||||
Channel channel = getChannelTemplate();
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
|
||||
mCurrentSystemInformation));
|
||||
|
||||
@@ -750,6 +816,70 @@ public class SiteEditor extends GridPane
|
||||
return (long)(siteFrequency.getFrequency() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 control channel label.
|
||||
*/
|
||||
private Label getP25ControlLabel()
|
||||
{
|
||||
if(mP25ControlLabel == null)
|
||||
{
|
||||
mP25ControlLabel = new Label("Control");
|
||||
mP25ControlLabel.setVisible(false);
|
||||
}
|
||||
|
||||
return mP25ControlLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 control channel type (TDMA vs FDMA) selection buttons.
|
||||
*/
|
||||
private SegmentedButton getP25ControlSegmentedButton()
|
||||
{
|
||||
if(mP25ControlSegmentedButton == null)
|
||||
{
|
||||
mP25ControlSegmentedButton = new SegmentedButton(getFdmaControlToggleButton(), getTdmaControlToggleButton());
|
||||
mP25ControlSegmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
|
||||
mP25ControlSegmentedButton.getToggleGroup().selectedToggleProperty()
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
//Ensure that one button is always selected
|
||||
if(newValue == null)
|
||||
{
|
||||
oldValue.setSelected(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mP25ControlSegmentedButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 with FDMA (phase 1) control channel selection button.
|
||||
*/
|
||||
private ToggleButton getFdmaControlToggleButton()
|
||||
{
|
||||
if(mFdmaControlToggleButton == null)
|
||||
{
|
||||
mFdmaControlToggleButton = new ToggleButton("FDMA Phase 1");
|
||||
mFdmaControlToggleButton.setVisible(false);
|
||||
}
|
||||
|
||||
return mFdmaControlToggleButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 with TDMA (phase 2) control channel selection button.
|
||||
*/
|
||||
private ToggleButton getTdmaControlToggleButton()
|
||||
{
|
||||
if(mTdmaControlToggleButton == null)
|
||||
{
|
||||
mTdmaControlToggleButton = new ToggleButton("TDMA Phase 2");
|
||||
mTdmaControlToggleButton.setVisible(false);
|
||||
}
|
||||
|
||||
return mTdmaControlToggleButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashes the alias list combobox to let the user know that they must select an alias list
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
package io.github.dsheirer.gui.playlist.radioreference;
|
||||
|
||||
import com.google.common.collect.Ordering;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Flavor;
|
||||
@@ -42,6 +43,8 @@ import javafx.scene.layout.Priority;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -126,6 +129,7 @@ public class SystemSiteSelectionEditor extends GridPane
|
||||
getFlavorLabel().setText(flavor != null ? flavor.getName() : null);
|
||||
Voice voice = decoder.getVoice(system);
|
||||
getVoiceLabel().setText(voice != null ? voice.getName() : null);
|
||||
Collections.sort(sites, Ordering.natural());
|
||||
getSiteTableView().getItems().addAll(sites);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -212,27 +216,33 @@ public class SystemSiteSelectionEditor extends GridPane
|
||||
mSiteTableView = new TableView<>();
|
||||
mSiteTableView.setPlaceholder(getPlaceholderLabel());
|
||||
|
||||
TableColumn numberColumn = new TableColumn();
|
||||
numberColumn.setText("Site");
|
||||
numberColumn.setCellValueFactory(new PropertyValueFactory<>("siteNumber"));
|
||||
numberColumn.setPrefWidth(75);
|
||||
TableColumn systemColumn = new TableColumn();
|
||||
systemColumn.setText("System");
|
||||
systemColumn.setCellValueFactory(new PropertyValueFactory<>("systemFormatted"));
|
||||
systemColumn.setPrefWidth(60);
|
||||
|
||||
TableColumn rfssColumn = new TableColumn();
|
||||
rfssColumn.setText("RFSS");
|
||||
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfss"));
|
||||
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfssFormatted"));
|
||||
rfssColumn.setPrefWidth(60);
|
||||
|
||||
TableColumn countyColumn = new TableColumn();
|
||||
countyColumn.setText("County");
|
||||
countyColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
|
||||
countyColumn.setPrefWidth(125);
|
||||
TableColumn siteColumn = new TableColumn();
|
||||
siteColumn.setText("Site");
|
||||
siteColumn.setCellValueFactory(new PropertyValueFactory<>("siteFormatted"));
|
||||
siteColumn.setPrefWidth(75);
|
||||
|
||||
TableColumn countyNameColumn = new TableColumn();
|
||||
countyNameColumn.setText("County");
|
||||
countyNameColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
|
||||
countyNameColumn.setPrefWidth(125);
|
||||
|
||||
TableColumn descriptionColumn = new TableColumn();
|
||||
descriptionColumn.setText("Name");
|
||||
descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description"));
|
||||
descriptionColumn.setPrefWidth(400);
|
||||
|
||||
mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
|
||||
mSiteTableView.getColumns().addAll(systemColumn, rfssColumn, siteColumn, countyNameColumn,
|
||||
descriptionColumn);
|
||||
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
mSiteTableView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, selected) ->
|
||||
|
||||
@@ -440,7 +440,10 @@ public class StreamingEditor extends SplitPane
|
||||
TableColumn stateColumn = new TableColumn("Stream Status");
|
||||
stateColumn.setCellValueFactory(new PropertyValueFactory<>("broadcastState"));
|
||||
|
||||
mConfiguredBroadcastTableView.getColumns().addAll(enabledColumn, nameColumn, typeColumn, stateColumn);
|
||||
TableColumn errorColumn = new TableColumn("Last Error");
|
||||
errorColumn.setCellValueFactory(new PropertyValueFactory<>("lastBadBroadcastState"));
|
||||
|
||||
mConfiguredBroadcastTableView.getColumns().addAll(enabledColumn, nameColumn, typeColumn, stateColumn, errorColumn);
|
||||
|
||||
mConfiguredBroadcastTableView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> setBroadcastConfiguration(newValue));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -444,11 +444,11 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
|
||||
|
||||
if(channel != null)
|
||||
{
|
||||
TunerChannel tunerChannel = channel.getTunerChannel();
|
||||
List<TunerChannel> tunerChannels = channel.getTunerChannels();
|
||||
|
||||
if(tunerChannel != null)
|
||||
if(!tunerChannels.isEmpty())
|
||||
{
|
||||
mFrequencyOverlayPanel.setChannelBandwidth(tunerChannel.getBandwidth());
|
||||
mFrequencyOverlayPanel.setChannelBandwidth(tunerChannels.get(0).getBandwidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ package io.github.dsheirer.gui.preference;
|
||||
|
||||
import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.call.CallManagementPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.duplicate.DuplicateCallPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.mp3.MP3PreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
|
||||
@@ -42,8 +42,8 @@ public class PreferenceEditorFactory
|
||||
{
|
||||
case APPLICATION:
|
||||
return new ApplicationPreferenceEditor(userPreferences);
|
||||
case AUDIO_DUPLICATE_CALL_DETECTION:
|
||||
return new DuplicateCallPreferenceEditor(userPreferences);
|
||||
case AUDIO_CALL_MANAGEMENT:
|
||||
return new CallManagementPreferenceEditor(userPreferences);
|
||||
case AUDIO_MP3:
|
||||
return new MP3PreferenceEditor(userPreferences);
|
||||
case AUDIO_OUTPUT:
|
||||
|
||||
@@ -31,7 +31,7 @@ public enum PreferenceEditorType
|
||||
AUDIO_MP3("MP3"),
|
||||
AUDIO_RECORD("Record"),
|
||||
AUDIO_OUTPUT("Output/Tones"),
|
||||
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
|
||||
AUDIO_CALL_MANAGEMENT("Call Management"),
|
||||
SOURCE_TUNERS("Tuners"),
|
||||
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
||||
VECTOR_CALIBRATION("Vector Calibration"),
|
||||
|
||||
@@ -171,7 +171,7 @@ public class UserPreferencesEditor extends BorderPane
|
||||
applicationItem.setExpanded(true);
|
||||
|
||||
TreeItem<String> audioItem = new TreeItem<>("Audio");
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_DUPLICATE_CALL_DETECTION));
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_CALL_MANAGEMENT));
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_MP3));
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_OUTPUT));
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,10 +23,16 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.application.ApplicationPreference;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
|
||||
/**
|
||||
@@ -38,6 +44,7 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
private GridPane mEditorPane;
|
||||
private Label mAutoStartTimeoutLabel;
|
||||
private Spinner<Integer> mTimeoutSpinner;
|
||||
private ToggleSwitch mAutomaticDiagnosticMonitoringToggle;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -46,7 +53,14 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
public ApplicationPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mApplicationPreference = userPreferences.getApplicationPreference();
|
||||
getChildren().add(getEditorPane());
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.setMaxHeight(Double.MAX_VALUE);
|
||||
vbox.setMaxWidth(Double.MAX_VALUE);
|
||||
vbox.getChildren().add(getEditorPane());
|
||||
HBox.setHgrow(vbox, Priority.ALWAYS);
|
||||
getChildren().add(vbox);
|
||||
}
|
||||
|
||||
private GridPane getEditorPane()
|
||||
@@ -55,13 +69,31 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
{
|
||||
int row = 0;
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setMaxWidth(Double.MAX_VALUE);
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(10);
|
||||
mEditorPane.setHgap(3);
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
GridPane.setHalignment(getAutoStartTimeoutLabel(), HPos.RIGHT);
|
||||
mEditorPane.add(getAutoStartTimeoutLabel(), 0, row);
|
||||
mEditorPane.add(getTimeoutSpinner(), 1, row);
|
||||
mEditorPane.add(new Label("seconds"), 2, row);
|
||||
|
||||
Label monitoringLabel = new Label("Application Health and Diagnostic Monitoring.");
|
||||
mEditorPane.add(monitoringLabel, 0, row, 2, 1);
|
||||
GridPane.setHalignment(getAutomaticDiagnosticMonitoringToggle(), HPos.RIGHT);
|
||||
mEditorPane.add(getAutomaticDiagnosticMonitoringToggle(), 0, ++row);
|
||||
mEditorPane.add(new Label("Enable Diagnostic Monitoring"), 1, row, 2, 1);
|
||||
|
||||
Separator separator = new Separator(Orientation.HORIZONTAL);
|
||||
GridPane.setHgrow(separator, Priority.ALWAYS);
|
||||
mEditorPane.add(separator, 0, ++row, 3, 1);
|
||||
|
||||
mEditorPane.add(getAutoStartTimeoutLabel(), 0, ++row, 2, 1);
|
||||
GridPane.setHalignment(getTimeoutSpinner(), HPos.RIGHT);
|
||||
mEditorPane.add(getTimeoutSpinner(), 0, ++row);
|
||||
mEditorPane.add(new Label("seconds"), 1, row);
|
||||
|
||||
ColumnConstraints c1 = new ColumnConstraints();
|
||||
c1.setPercentWidth(30);
|
||||
ColumnConstraints c2 = new ColumnConstraints();
|
||||
c2.setHgrow(Priority.ALWAYS);
|
||||
mEditorPane.getColumnConstraints().addAll(c1, c2);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
@@ -91,4 +123,20 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
|
||||
return mTimeoutSpinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch to enable/disable automatic diagnostic monitoring.
|
||||
*/
|
||||
private ToggleSwitch getAutomaticDiagnosticMonitoringToggle()
|
||||
{
|
||||
if(mAutomaticDiagnosticMonitoringToggle == null)
|
||||
{
|
||||
mAutomaticDiagnosticMonitoringToggle = new ToggleSwitch();
|
||||
mAutomaticDiagnosticMonitoringToggle.setSelected(mApplicationPreference.isAutomaticDiagnosticMonitoring());
|
||||
mAutomaticDiagnosticMonitoringToggle.selectedProperty().addListener((observable, oldValue, enabled) ->
|
||||
mApplicationPreference.setAutomaticDiagnosticMonitoring(enabled));
|
||||
}
|
||||
|
||||
return mAutomaticDiagnosticMonitoringToggle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -17,12 +17,16 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.duplicate;
|
||||
package io.github.dsheirer.gui.preference.call;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.PatchGroupStreamingOption;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
|
||||
import io.github.dsheirer.preference.duplicate.CallManagementPreference;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.layout.GridPane;
|
||||
@@ -33,25 +37,26 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Preference settings for duplicate call audio handling
|
||||
* Preference settings for audio call management for duplicate calls and patch group streaming.
|
||||
*/
|
||||
public class DuplicateCallPreferenceEditor extends HBox
|
||||
public class CallManagementPreferenceEditor extends HBox
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallPreferenceEditor.class);
|
||||
private DuplicateCallDetectionPreference mPreference;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(CallManagementPreferenceEditor.class);
|
||||
private CallManagementPreference mPreference;
|
||||
private GridPane mEditorPane;
|
||||
private ToggleSwitch mDetectDuplicateTalkgroups;
|
||||
private ToggleSwitch mDetectDuplicateRadios;
|
||||
private ToggleSwitch mSuppressDuplicateListening;
|
||||
private ToggleSwitch mSuppressDuplicateRecording;
|
||||
private ToggleSwitch mSuppressDuplicateStreaming;
|
||||
private ComboBox<PatchGroupStreamingOption> mPatchGroupStreamingOptionComboBox;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DuplicateCallPreferenceEditor(UserPreferences userPreferences)
|
||||
public CallManagementPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
mPreference = userPreferences.getCallManagementPreference();
|
||||
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
getChildren().add(getEditorPane());
|
||||
@@ -134,6 +139,19 @@ public class DuplicateCallPreferenceEditor extends HBox
|
||||
streamingLabel.setWrapText(true);
|
||||
GridPane.setConstraints(streamingLabel, 1, row);
|
||||
mEditorPane.getChildren().add(streamingLabel);
|
||||
|
||||
Separator separator2 = new Separator();
|
||||
GridPane.setHgrow(separator2, Priority.ALWAYS);
|
||||
GridPane.setConstraints(separator2, 0, ++row, 2, 1);
|
||||
mEditorPane.getChildren().add(separator2);
|
||||
|
||||
Label patchGroupLabel = new Label("Patch Group Streaming. Stream a patch group call as:");
|
||||
patchGroupLabel.setWrapText(true);
|
||||
GridPane.setConstraints(patchGroupLabel, 0, ++row, 2, 1);
|
||||
mEditorPane.getChildren().add(patchGroupLabel);
|
||||
|
||||
GridPane.setConstraints(getPatchGroupStreamingOptionComboBox(), 0, ++row, 2, 1);
|
||||
mEditorPane.getChildren().add(getPatchGroupStreamingOptionComboBox());
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
@@ -212,4 +230,23 @@ public class DuplicateCallPreferenceEditor extends HBox
|
||||
|
||||
return mSuppressDuplicateStreaming;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combo box for presenting the patch group streaming options.
|
||||
* @return combo box.
|
||||
*/
|
||||
private ComboBox<PatchGroupStreamingOption> getPatchGroupStreamingOptionComboBox()
|
||||
{
|
||||
if(mPatchGroupStreamingOptionComboBox == null)
|
||||
{
|
||||
ObservableList<PatchGroupStreamingOption> options = FXCollections.observableArrayList();
|
||||
options.addAll(PatchGroupStreamingOption.values());
|
||||
mPatchGroupStreamingOptionComboBox = new ComboBox<>(options);
|
||||
mPatchGroupStreamingOptionComboBox.getSelectionModel().select(mPreference.getPatchGroupStreamingOption());
|
||||
mPatchGroupStreamingOptionComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPreference.setPatchGroupStreamingOption(newValue));
|
||||
}
|
||||
|
||||
return mPatchGroupStreamingOptionComboBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
/**
|
||||
* Viewer for channel start processing requests
|
||||
*/
|
||||
public class ChannelStartProcessingRequestViewer extends HBox
|
||||
{
|
||||
private Label mChannelConfigLabel;
|
||||
private Label mChannelDescriptorLabel;
|
||||
private Label mTrafficChannelManagerLabel;
|
||||
private Label mPreloadDataContentLabel;
|
||||
private Label mParentDecodeHistoryLabel;
|
||||
private Label mChildDecodeHistoryLabel;
|
||||
private IdentifierCollectionViewer mIdentifierCollectionViewer;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public ChannelStartProcessingRequestViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setVgap(5);
|
||||
gridPane.setHgap(5);
|
||||
|
||||
gridPane.add(new Label("Configuration:"), 0, 0);
|
||||
GridPane.setHgrow(getChannelConfigLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChannelConfigLabel(), 1, 0);
|
||||
|
||||
gridPane.add(new Label("Channel:"), 0, 1);
|
||||
GridPane.setHgrow(getChannelDescriptorLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChannelDescriptorLabel(), 1, 1);
|
||||
|
||||
gridPane.add(new Label("TCM:"), 0, 2);
|
||||
GridPane.setHgrow(getTrafficChannelManagerLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getTrafficChannelManagerLabel(), 1, 2);
|
||||
|
||||
gridPane.add(new Label("Preload Items:"), 0, 3);
|
||||
GridPane.setHgrow(getPreloadDataContentLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getPreloadDataContentLabel(), 1, 3);
|
||||
|
||||
gridPane.add(new Label("Parent History:"), 0, 4);
|
||||
GridPane.setHgrow(getParentDecodeHistoryLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getParentDecodeHistoryLabel(), 1, 4);
|
||||
|
||||
gridPane.add(new Label("Child History:"), 0, 5);
|
||||
GridPane.setHgrow(getChildDecodeHistoryLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChildDecodeHistoryLabel(), 1, 5);
|
||||
|
||||
gridPane.add(new Label("Identifiers"), 0, 6);
|
||||
GridPane.setHgrow(getIdentifierCollectionViewer(), Priority.ALWAYS);
|
||||
getIdentifierCollectionViewer().setPrefHeight(120);
|
||||
gridPane.add(getIdentifierCollectionViewer(), 0, 7, 2, 1);
|
||||
|
||||
ColumnConstraints cc0 = new ColumnConstraints();
|
||||
cc0.setHalignment(HPos.RIGHT);
|
||||
gridPane.getColumnConstraints().add(cc0);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(ChannelStartProcessingRequest request)
|
||||
{
|
||||
if(request != null)
|
||||
{
|
||||
if(request.getChannel() != null)
|
||||
{
|
||||
getChannelConfigLabel().setText(request.getChannel().toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelConfigLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getChannelDescriptor() != null)
|
||||
{
|
||||
getChannelDescriptorLabel().setText(request.getChannelDescriptor().toString() + " " +
|
||||
request.getChannelDescriptor().getDownlinkFrequency() + " MHz");
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelDescriptorLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getChildDecodeEventHistory() != null)
|
||||
{
|
||||
getChildDecodeHistoryLabel().setText(request.getChildDecodeEventHistory().getItems().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getChildDecodeHistoryLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getParentDecodeEventHistory() != null)
|
||||
{
|
||||
getParentDecodeHistoryLabel().setText(request.getParentDecodeEventHistory().getItems().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getParentDecodeHistoryLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getPreloadDataContents() != null)
|
||||
{
|
||||
getPreloadDataContentLabel().setText(request.getPreloadDataContents().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getPreloadDataContentLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getTrafficChannelManager() != null)
|
||||
{
|
||||
getTrafficChannelManagerLabel().setText(request.getTrafficChannelManager().getClass().getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
getTrafficChannelManagerLabel().setText("None:");
|
||||
}
|
||||
|
||||
if(request.getIdentifierCollection() != null)
|
||||
{
|
||||
getIdentifierCollectionViewer().set(request.getIdentifierCollection());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelDescriptorLabel().setText(null);
|
||||
getChildDecodeHistoryLabel().setText(null);
|
||||
getChannelConfigLabel().setText(null);
|
||||
getIdentifierCollectionViewer().set(null);
|
||||
getParentDecodeHistoryLabel().setText(null);
|
||||
getPreloadDataContentLabel().setText(null);
|
||||
getTrafficChannelManagerLabel().setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Label getChannelConfigLabel()
|
||||
{
|
||||
if(mChannelConfigLabel == null)
|
||||
{
|
||||
mChannelConfigLabel = new Label();
|
||||
}
|
||||
|
||||
return mChannelConfigLabel;
|
||||
}
|
||||
|
||||
public Label getChannelDescriptorLabel()
|
||||
{
|
||||
if(mChannelDescriptorLabel == null)
|
||||
{
|
||||
mChannelDescriptorLabel = new Label();
|
||||
}
|
||||
|
||||
return mChannelDescriptorLabel;
|
||||
}
|
||||
|
||||
private Label getTrafficChannelManagerLabel()
|
||||
{
|
||||
if(mTrafficChannelManagerLabel == null)
|
||||
{
|
||||
mTrafficChannelManagerLabel = new Label();
|
||||
|
||||
}
|
||||
|
||||
return mTrafficChannelManagerLabel;
|
||||
}
|
||||
|
||||
private Label getPreloadDataContentLabel()
|
||||
{
|
||||
if(mPreloadDataContentLabel == null)
|
||||
{
|
||||
mPreloadDataContentLabel = new Label();
|
||||
}
|
||||
|
||||
return mPreloadDataContentLabel;
|
||||
}
|
||||
|
||||
private Label getParentDecodeHistoryLabel()
|
||||
{
|
||||
if(mParentDecodeHistoryLabel == null)
|
||||
{
|
||||
mParentDecodeHistoryLabel = new Label();
|
||||
}
|
||||
|
||||
return mParentDecodeHistoryLabel;
|
||||
}
|
||||
|
||||
private Label getChildDecodeHistoryLabel()
|
||||
{
|
||||
if(mChildDecodeHistoryLabel == null)
|
||||
{
|
||||
mChildDecodeHistoryLabel = new Label();
|
||||
}
|
||||
|
||||
return mChildDecodeHistoryLabel;
|
||||
}
|
||||
|
||||
private IdentifierCollectionViewer getIdentifierCollectionViewer()
|
||||
{
|
||||
if(mIdentifierCollectionViewer == null)
|
||||
{
|
||||
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
|
||||
}
|
||||
|
||||
return mIdentifierCollectionViewer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -46,6 +46,7 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TablePosition;
|
||||
import javafx.scene.control.TableView;
|
||||
@@ -284,6 +285,7 @@ public class DmrViewer extends VBox
|
||||
if(mMessageTableView == null)
|
||||
{
|
||||
mMessageTableView = new TableView<>();
|
||||
mMessageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
mMessageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
||||
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* JavaFX identifier collection viewer
|
||||
*/
|
||||
public class IdentifierCollectionViewer extends VBox
|
||||
{
|
||||
private TableView<Identifier> mIdentifierTableView;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public IdentifierCollectionViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
GridPane.setHgrow(getIdentifierTableView(), Priority.ALWAYS);
|
||||
gridPane.add(getIdentifierTableView(), 0, 0);
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(IdentifierCollection identifierCollection)
|
||||
{
|
||||
getIdentifierTableView().getItems().clear();
|
||||
|
||||
if(identifierCollection != null)
|
||||
{
|
||||
getIdentifierTableView().getItems().addAll(identifierCollection.getIdentifiers());
|
||||
}
|
||||
}
|
||||
|
||||
public TableView<Identifier> getIdentifierTableView()
|
||||
{
|
||||
if(mIdentifierTableView == null)
|
||||
{
|
||||
mIdentifierTableView = new TableView<>();
|
||||
|
||||
TableColumn classColumn = new TableColumn();
|
||||
classColumn.setPrefWidth(110);
|
||||
classColumn.setText("Class");
|
||||
classColumn.setCellValueFactory(new PropertyValueFactory<>("identifierClass"));
|
||||
|
||||
TableColumn formColumn = new TableColumn();
|
||||
formColumn.setPrefWidth(160);
|
||||
formColumn.setText("Form");
|
||||
formColumn.setCellValueFactory(new PropertyValueFactory<>("form"));
|
||||
|
||||
TableColumn roleColumn = new TableColumn();
|
||||
roleColumn.setPrefWidth(110);
|
||||
roleColumn.setText("Role");
|
||||
roleColumn.setCellValueFactory(new PropertyValueFactory<>("role"));
|
||||
|
||||
TableColumn valueColumn = new TableColumn();
|
||||
valueColumn.setPrefWidth(160);
|
||||
valueColumn.setText("Value");
|
||||
valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
|
||||
|
||||
TableColumn textColumn = new TableColumn();
|
||||
textColumn.setPrefWidth(160);
|
||||
textColumn.setText("Text");
|
||||
textColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures<Identifier<?>, String>, ObservableValue>) param -> {
|
||||
if(param.getValue() != null)
|
||||
{
|
||||
return new SimpleStringProperty(param.getValue().toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
mIdentifierTableView.getColumns().addAll(classColumn, formColumn, roleColumn, valueColumn, textColumn);
|
||||
}
|
||||
|
||||
return mIdentifierTableView;
|
||||
}
|
||||
}
|
||||
191
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal file
191
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.AudioSegment;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wrapper class that combines a single message with any decoder state events and decoded events that were produced by a
|
||||
* decoder state.
|
||||
*/
|
||||
public class MessagePackage
|
||||
{
|
||||
private IMessage mMessage;
|
||||
private List<DecoderStateEvent> mDecoderStateEvents = new ArrayList<>();
|
||||
private List<DecodeEventSnapshot> mDecodeEvents = new ArrayList<>();
|
||||
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
|
||||
private AudioSegment mAudioSegment;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param message for this instance
|
||||
*/
|
||||
public MessagePackage(IMessage message)
|
||||
{
|
||||
mMessage = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message for this instance
|
||||
*/
|
||||
public IMessage getMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of decoder state events
|
||||
*/
|
||||
public List<DecoderStateEvent> getDecoderStateEvents()
|
||||
{
|
||||
return mDecoderStateEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decoder state event to this instance
|
||||
*/
|
||||
public void add(DecoderStateEvent event)
|
||||
{
|
||||
mDecoderStateEvents.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of decode event snapshots
|
||||
*/
|
||||
public List<DecodeEventSnapshot> getDecodeEvents()
|
||||
{
|
||||
return mDecodeEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decode event to this instance
|
||||
*/
|
||||
public void add(DecodeEventSnapshot event)
|
||||
{
|
||||
mDecodeEvents.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the channel start processing request
|
||||
*/
|
||||
public void add(ChannelStartProcessingRequest request)
|
||||
{
|
||||
mChannelStartProcessingRequest = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message timeslot
|
||||
*/
|
||||
public int getTimeslot()
|
||||
{
|
||||
return getMessage().getTimeslot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message timestamp
|
||||
*/
|
||||
public long getTimestamp()
|
||||
{
|
||||
return getMessage().getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message validity flag
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return getMessage().isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message string representation
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getMessage().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of decode events
|
||||
*/
|
||||
public int getDecodeEventCount()
|
||||
{
|
||||
return mDecodeEvents.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of decoder state events
|
||||
*/
|
||||
public int getDecoderStateEventCount()
|
||||
{
|
||||
return mDecoderStateEvents.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count (0 or 1) of channel start processing requests.
|
||||
*/
|
||||
public int getChannelStartProcessingRequestCount()
|
||||
{
|
||||
return mChannelStartProcessingRequest == null ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count (0 or 1) of audio segment.
|
||||
*/
|
||||
public int getAudioSegmentCount()
|
||||
{
|
||||
return mAudioSegment == null ? 0 : 1;
|
||||
}
|
||||
|
||||
public ChannelStartProcessingRequest getChannelStartProcessingRequest()
|
||||
{
|
||||
return mChannelStartProcessingRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated audio segment.
|
||||
* @return segment or null.
|
||||
*/
|
||||
public AudioSegment getAudioSegment()
|
||||
{
|
||||
return mAudioSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the audio segment to the package
|
||||
* @param audioSegment to add
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
if(mAudioSegment != null)
|
||||
{
|
||||
throw new IllegalStateException("AudioSegment already set");
|
||||
}
|
||||
|
||||
mAudioSegment = audioSegment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* JavaFX message package details viewer
|
||||
*/
|
||||
public class MessagePackageViewer extends VBox
|
||||
{
|
||||
private Label mMessageLabel;
|
||||
private TableView<DecoderStateEvent> mDecoderStateEventTableView;
|
||||
private TableView<DecodeEventSnapshot> mDecodeEventTableView;
|
||||
private IdentifierCollectionViewer mIdentifierCollectionViewer;
|
||||
private IdentifierCollectionViewer mAudioSegmentIdCollectionViewer;
|
||||
private ChannelStartProcessingRequestViewer mChannelStartProcessingRequestViewer;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public MessagePackageViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(5);
|
||||
Label messageLabel = new Label("Message:");
|
||||
gridPane.add(messageLabel, 0, 0);
|
||||
GridPane.setHgrow(getMessageLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getMessageLabel(), 1, 0);
|
||||
|
||||
gridPane.add(new Label("Decoder State Events"), 0, 1);
|
||||
getDecoderStateEventTableView().setPrefHeight(120);
|
||||
GridPane.setHgrow(getDecoderStateEventTableView(), Priority.NEVER);
|
||||
gridPane.add(getDecoderStateEventTableView(), 0, 2);
|
||||
|
||||
gridPane.add(new Label("Decode Events"), 1, 1);
|
||||
getDecodeEventTableView().setPrefHeight(120);
|
||||
GridPane.setHgrow(getDecodeEventTableView(), Priority.ALWAYS);
|
||||
gridPane.add(getDecodeEventTableView(), 1, 2);
|
||||
|
||||
TabPane tabPane = new TabPane();
|
||||
Tab startTab = new Tab("Channel Start Processing Request");
|
||||
startTab.setContent(getChannelStartProcessingRequestViewer());
|
||||
Tab audioTab = new Tab("Audio Segment");
|
||||
audioTab.setContent(getAudioSegmentIdCollectionViewer());
|
||||
tabPane.getTabs().addAll(audioTab, startTab);
|
||||
|
||||
gridPane.add(tabPane, 0, 4);
|
||||
|
||||
getIdentifierCollectionViewer().setPrefHeight(120);
|
||||
gridPane.add(new Label("Selected Decode Event Identifiers"), 1, 3);
|
||||
gridPane.add(getIdentifierCollectionViewer(), 1, 4);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(MessagePackage messagePackage)
|
||||
{
|
||||
getMessageLabel().setText(null);
|
||||
getDecoderStateEventTableView().getItems().clear();
|
||||
getDecodeEventTableView().getItems().clear();
|
||||
getChannelStartProcessingRequestViewer().set(null);
|
||||
|
||||
if(messagePackage != null)
|
||||
{
|
||||
String message = messagePackage.toString();
|
||||
if(message.length() > 40)
|
||||
{
|
||||
message = message.substring(0, 40) + "...";
|
||||
}
|
||||
getMessageLabel().setText(message);
|
||||
getDecoderStateEventTableView().getItems().addAll(messagePackage.getDecoderStateEvents());
|
||||
getDecodeEventTableView().getItems().addAll(messagePackage.getDecodeEvents());
|
||||
getChannelStartProcessingRequestViewer().set(messagePackage.getChannelStartProcessingRequest());
|
||||
|
||||
if(getDecodeEventTableView().getItems().size() > 0)
|
||||
{
|
||||
getDecodeEventTableView().getSelectionModel().select(0);
|
||||
}
|
||||
|
||||
if(messagePackage.getAudioSegment() != null)
|
||||
{
|
||||
getAudioSegmentIdCollectionViewer().set(messagePackage.getAudioSegment().getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
getAudioSegmentIdCollectionViewer().set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IdentifierCollectionViewer getIdentifierCollectionViewer()
|
||||
{
|
||||
if(mIdentifierCollectionViewer == null)
|
||||
{
|
||||
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
|
||||
}
|
||||
|
||||
return mIdentifierCollectionViewer;
|
||||
}
|
||||
|
||||
private IdentifierCollectionViewer getAudioSegmentIdCollectionViewer()
|
||||
{
|
||||
if(mAudioSegmentIdCollectionViewer == null)
|
||||
{
|
||||
mAudioSegmentIdCollectionViewer = new IdentifierCollectionViewer();
|
||||
}
|
||||
|
||||
return mAudioSegmentIdCollectionViewer;
|
||||
}
|
||||
|
||||
private Label getMessageLabel()
|
||||
{
|
||||
if(mMessageLabel == null)
|
||||
{
|
||||
mMessageLabel = new Label();
|
||||
mMessageLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
return mMessageLabel;
|
||||
}
|
||||
|
||||
public TableView<DecoderStateEvent> getDecoderStateEventTableView()
|
||||
{
|
||||
if(mDecoderStateEventTableView == null)
|
||||
{
|
||||
mDecoderStateEventTableView = new TableView<>();
|
||||
|
||||
TableColumn timeslotColumn = new TableColumn();
|
||||
timeslotColumn.setPrefWidth(110);
|
||||
timeslotColumn.setText("Timeslot");
|
||||
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
|
||||
|
||||
TableColumn stateColumn = new TableColumn();
|
||||
stateColumn.setPrefWidth(110);
|
||||
stateColumn.setText("State");
|
||||
stateColumn.setCellValueFactory(new PropertyValueFactory<>("state"));
|
||||
|
||||
TableColumn eventColumn = new TableColumn();
|
||||
eventColumn.setPrefWidth(110);
|
||||
eventColumn.setText("Event");
|
||||
eventColumn.setCellValueFactory(new PropertyValueFactory<>("event"));
|
||||
|
||||
TableColumn frequencyColumn = new TableColumn();
|
||||
frequencyColumn.setPrefWidth(110);
|
||||
frequencyColumn.setText("Frequency");
|
||||
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
|
||||
|
||||
mDecoderStateEventTableView.getColumns().addAll(timeslotColumn, stateColumn, eventColumn, frequencyColumn);
|
||||
}
|
||||
|
||||
return mDecoderStateEventTableView;
|
||||
}
|
||||
|
||||
private TableView<DecodeEventSnapshot> getDecodeEventTableView()
|
||||
{
|
||||
if(mDecodeEventTableView == null)
|
||||
{
|
||||
mDecodeEventTableView = new TableView<>();
|
||||
mDecodeEventTableView.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
TableColumn startTimeColumn = new TableColumn();
|
||||
startTimeColumn.setPrefWidth(110);
|
||||
startTimeColumn.setText("Start");
|
||||
startTimeColumn.setCellValueFactory(new PropertyValueFactory<>("timeStart"));
|
||||
|
||||
TableColumn durationColumn = new TableColumn();
|
||||
durationColumn.setPrefWidth(110);
|
||||
durationColumn.setText("Duration");
|
||||
durationColumn.setCellValueFactory(new PropertyValueFactory<>("duration"));
|
||||
|
||||
TableColumn typeColumn = new TableColumn();
|
||||
typeColumn.setPrefWidth(130);
|
||||
typeColumn.setText("Type");
|
||||
typeColumn.setCellValueFactory(new PropertyValueFactory<>("eventType"));
|
||||
|
||||
TableColumn channelDescriptorColumn = new TableColumn();
|
||||
channelDescriptorColumn.setPrefWidth(110);
|
||||
channelDescriptorColumn.setText("Channel");
|
||||
channelDescriptorColumn.setCellValueFactory(new PropertyValueFactory<>("channelDescriptor"));
|
||||
|
||||
TableColumn frequencyColumn = new TableColumn();
|
||||
frequencyColumn.setPrefWidth(110);
|
||||
frequencyColumn.setText("Frequency");
|
||||
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
|
||||
|
||||
TableColumn hashcodeColumn = new TableColumn();
|
||||
hashcodeColumn.setPrefWidth(100);
|
||||
hashcodeColumn.setText("Hash ID");
|
||||
hashcodeColumn.setCellValueFactory(new PropertyValueFactory<>("originalHashCode"));
|
||||
|
||||
TableColumn detailsColumn = new TableColumn();
|
||||
detailsColumn.setPrefWidth(500);
|
||||
detailsColumn.setText("Details");
|
||||
detailsColumn.setCellValueFactory(new PropertyValueFactory<>("details"));
|
||||
|
||||
mDecodeEventTableView.getColumns().addAll(startTimeColumn, durationColumn, typeColumn, channelDescriptorColumn,
|
||||
frequencyColumn, hashcodeColumn, detailsColumn);
|
||||
|
||||
mDecodeEventTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
|
||||
{
|
||||
if(newValue != null)
|
||||
{
|
||||
getIdentifierCollectionViewer().set(newValue.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollectionViewer().set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mDecodeEventTableView;
|
||||
}
|
||||
|
||||
private ChannelStartProcessingRequestViewer getChannelStartProcessingRequestViewer()
|
||||
{
|
||||
if(mChannelStartProcessingRequestViewer == null)
|
||||
{
|
||||
mChannelStartProcessingRequestViewer = new ChannelStartProcessingRequestViewer();
|
||||
}
|
||||
|
||||
return mChannelStartProcessingRequestViewer;
|
||||
}
|
||||
}
|
||||
119
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal file
119
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
|
||||
/**
|
||||
* Utility for combining a message and decoder state events.
|
||||
*/
|
||||
public class MessagePackager
|
||||
{
|
||||
private MessagePackage mMessagePackage;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public MessagePackager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio segment.
|
||||
* @param audioSegment to add
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
if(mMessagePackage != null)
|
||||
{
|
||||
mMessagePackage.add(audioSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the message and creates a new MessageWithEvents instance, wrapping the message, ready to also receive any
|
||||
* decode events and decoder state events. The previous message with events is overwritten.
|
||||
* @param message to wrap.
|
||||
*/
|
||||
public void add(IMessage message)
|
||||
{
|
||||
mMessagePackage = new MessagePackage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the current message with events.
|
||||
*/
|
||||
public MessagePackage getMessageWithEvents()
|
||||
{
|
||||
return mMessagePackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decoder state event to the current message with events.
|
||||
* @param event to add
|
||||
*/
|
||||
public void add(DecoderStateEvent event)
|
||||
{
|
||||
if(mMessagePackage != null)
|
||||
{
|
||||
mMessagePackage.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decode event to the current message with events.
|
||||
* @param event to add
|
||||
*/
|
||||
public void add(IDecodeEvent event)
|
||||
{
|
||||
if(mMessagePackage != null)
|
||||
{
|
||||
if(event instanceof DecodeEvent decodeEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
DecodeEventSnapshot snapshot = decodeEvent.getSnapshot();
|
||||
mMessagePackage.add(snapshot);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription to receive channel start processing requests via the event bus from the traffic channel manager
|
||||
* @param request sent from the traffic channel manager.
|
||||
*/
|
||||
@Subscribe
|
||||
public void process(ChannelStartProcessingRequest request)
|
||||
{
|
||||
mMessagePackage.add(request);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.gui.viewer;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
@@ -41,20 +42,22 @@ 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.
|
||||
* Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
|
||||
*/
|
||||
public class RecordingViewer extends VBox
|
||||
public class MessageRecordingViewer extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
|
||||
private static final Logger mLog = LoggerFactory.getLogger(MessageRecordingViewer.class);
|
||||
private MenuBar mMenuBar;
|
||||
private TabPane mTabPane;
|
||||
private int mTabCounterDmr = 1;
|
||||
private int mTabCounterP25P1 = 1;
|
||||
private int mTabCounterP25P2 = 1;
|
||||
private UserPreferences mUserPreferences = new UserPreferences();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public RecordingViewer()
|
||||
public MessageRecordingViewer()
|
||||
{
|
||||
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
|
||||
getChildren().addAll(getMenuBar(), getTabPane());
|
||||
@@ -74,13 +77,19 @@ public class RecordingViewer extends VBox
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
|
||||
p25p1MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences));
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
|
||||
MenuItem p25p2MenuItem = new MenuItem("P25 Phase 2");
|
||||
p25p2MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
|
||||
|
||||
MenuItem exitMenu = new MenuItem("Exit");
|
||||
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
|
||||
@@ -101,7 +110,8 @@ public class RecordingViewer extends VBox
|
||||
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()));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
|
||||
}
|
||||
|
||||
return mTabPane;
|
||||
@@ -151,11 +161,6 @@ public class RecordingViewer extends VBox
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LabeledTab(String label)
|
||||
{
|
||||
this(label, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
@@ -165,7 +170,7 @@ public class RecordingViewer extends VBox
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception
|
||||
{
|
||||
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
|
||||
Scene scene = new Scene(new MessageRecordingViewer(), 1100, 800);
|
||||
primaryStage.setTitle("Message Recording Viewer (.bits)");
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,11 +19,25 @@
|
||||
|
||||
package io.github.dsheirer.gui.viewer;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.SingleChannelState;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.StuffBitsMessage;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -33,8 +47,11 @@ import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -45,6 +62,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TablePosition;
|
||||
import javafx.scene.control.TableView;
|
||||
@@ -71,20 +89,25 @@ 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 static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
|
||||
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 TableView<MessagePackage> mMessagePackageTableView;
|
||||
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
|
||||
private FilteredList<MessagePackage> mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
|
||||
private TextField mSearchText;
|
||||
private TextField mFindText;
|
||||
private Button mFindButton;
|
||||
private Button mFindNextButton;
|
||||
private ProgressIndicator mLoadingIndicator;
|
||||
private MessagePackageViewer mMessagePackageViewer;
|
||||
private StringProperty mLoadedFile = new SimpleStringProperty();
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
public P25P1Viewer()
|
||||
public P25P1Viewer(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
setPadding(new Insets(5));
|
||||
setSpacing(5);
|
||||
|
||||
@@ -115,9 +138,11 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
VBox.setVgrow(fileBox, Priority.NEVER);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
|
||||
|
||||
getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
|
||||
|
||||
getChildren().addAll(fileBox, filterBox, getMessageTableView());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,22 +157,73 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
mMessages.clear();
|
||||
mLoadedFile.set(file.toString());
|
||||
mMessagePackages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
List<IMessage> messages = new ArrayList<>();
|
||||
List<MessagePackage> messagePackages = new ArrayList<>();
|
||||
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
|
||||
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
|
||||
messageFramer.setListener(messageProcessor);
|
||||
Broadcaster<IMessage> messageBroadcaster = new Broadcaster<>();
|
||||
messageFramer.setListener(messageBroadcaster);
|
||||
messageBroadcaster.addListener(messageProcessor);
|
||||
|
||||
Channel empty = new Channel("Empty");
|
||||
empty.setDecodeConfiguration(new DecodeConfigP25Phase1());
|
||||
|
||||
MessagePackager messagePackager = new MessagePackager();
|
||||
|
||||
//Setup a temporary event bus to capture channel start processing requests
|
||||
EventBus eventBus = new EventBus("debug");
|
||||
eventBus.register(messagePackager);
|
||||
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
|
||||
trafficChannelManager.setInterModuleEventBus(eventBus);
|
||||
|
||||
//Register to receive events
|
||||
trafficChannelManager.addDecodeEventListener(messagePackager::add);
|
||||
P25P1DecoderState decoderState = new P25P1DecoderState(empty, trafficChannelManager);
|
||||
|
||||
Broadcaster<DecoderStateEvent> decoderStateEventBroadcaster = new Broadcaster<>();
|
||||
decoderState.setDecoderStateListener(decoderStateEventBroadcaster);
|
||||
|
||||
decoderStateEventBroadcaster.addListener(messagePackager::add);
|
||||
decoderState.addDecodeEventListener(messagePackager::add);
|
||||
decoderState.start();
|
||||
|
||||
long frequency = getFrequencyFromFile(mLoadedFile.get());
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
|
||||
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
|
||||
decoderState.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 1));
|
||||
}
|
||||
|
||||
messageProcessor.setMessageListener(message -> {
|
||||
if(!(message instanceof StuffBitsMessage))
|
||||
{
|
||||
messages.add(message);
|
||||
//Add the initial message to the packager so that it can be combined with any decoder state events.
|
||||
messagePackager.add(message);
|
||||
decoderState.receive(message);
|
||||
|
||||
//Collect the packaged message with events
|
||||
messagePackages.add(messagePackager.getMessageWithEvents());
|
||||
}
|
||||
});
|
||||
|
||||
P25P1AudioModule audioModule = new P25P1AudioModule(mUserPreferences, new AliasList("debug"));
|
||||
decoderState.setIdentifierUpdateListener(audioModule.getIdentifierUpdateListener());
|
||||
audioModule.setAudioSegmentListener(messagePackager::add);
|
||||
messageBroadcaster.addListener(audioModule);
|
||||
audioModule.start();
|
||||
SingleChannelState singleChannelState = new SingleChannelState(empty, new AliasModel());
|
||||
singleChannelState.setSquelchStateListener(squelchStateEvent -> audioModule.getSquelchStateListener().receive(squelchStateEvent));
|
||||
decoderStateEventBroadcaster.addListener(singleChannelState.getDecoderStateListener());
|
||||
singleChannelState.start();
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
@@ -161,16 +237,54 @@ public class P25P1Viewer extends VBox
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
audioModule.stop();
|
||||
decoderState.stop();
|
||||
singleChannelState.stop();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getLoadingIndicator().setVisible(false);
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessages.addAll(messages);
|
||||
getMessageTableView().scrollTo(0);
|
||||
mMessagePackages.addAll(messagePackages);
|
||||
getMessagePackageTableView().scrollTo(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
|
||||
* the decoder states.
|
||||
* @param file name to parse
|
||||
* @return parsed frequency or zero.
|
||||
*/
|
||||
private static long getFrequencyFromFile(String file)
|
||||
{
|
||||
if(file == null || file.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(file.matches(FILE_FREQUENCY_REGEX))
|
||||
{
|
||||
Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
|
||||
Matcher m = p.matcher(file);
|
||||
if(m.find())
|
||||
{
|
||||
try
|
||||
{
|
||||
String raw = m.group(1);
|
||||
return Long.parseLong(raw);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't parse frequency from bits file [" + file + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the filter(s) applies to the list of messages
|
||||
*/
|
||||
@@ -180,12 +294,12 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
if(filterText != null && !filterText.isEmpty())
|
||||
{
|
||||
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessages.setPredicate(textPredicate);
|
||||
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessagePackages.setPredicate(textPredicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFilteredMessages.setPredicate(null);
|
||||
mFilteredMessagePackages.setPredicate(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +311,12 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
for(IMessage message: mFilteredMessages)
|
||||
for(MessagePackage messagePackage: mFilteredMessagePackages)
|
||||
{
|
||||
if(message.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessageTableView().getSelectionModel().select(message);
|
||||
getMessageTableView().scrollTo(message);
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -217,7 +331,7 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
|
||||
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected == null)
|
||||
{
|
||||
@@ -225,18 +339,18 @@ public class P25P1Viewer extends VBox
|
||||
return;
|
||||
}
|
||||
|
||||
int row = mFilteredMessages.indexOf(selected);
|
||||
int row = mFilteredMessagePackages.indexOf(selected);
|
||||
|
||||
for(int x = row + 1; x < mFilteredMessages.size(); x++)
|
||||
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
|
||||
{
|
||||
if(x < mFilteredMessages.size())
|
||||
if(x < mFilteredMessagePackages.size())
|
||||
{
|
||||
IMessage message = mFilteredMessages.get(x);
|
||||
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
|
||||
|
||||
if(message.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessageTableView().getSelectionModel().select(message);
|
||||
getMessageTableView().scrollTo(message);
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -244,25 +358,41 @@ public class P25P1Viewer extends VBox
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with DMR messages
|
||||
*/
|
||||
private TableView<IMessage> getMessageTableView()
|
||||
private MessagePackageViewer getMessagePackageViewer()
|
||||
{
|
||||
if(mMessageTableView == null)
|
||||
if(mMessagePackageViewer == null)
|
||||
{
|
||||
mMessageTableView = new TableView<>();
|
||||
mMessageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
||||
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
||||
mMessageTableView.setItems(sortedList);
|
||||
mMessagePackageViewer = new MessagePackageViewer();
|
||||
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
mMessageTableView.setOnKeyPressed(event ->
|
||||
//Register for table selection events to display the selected value.
|
||||
getMessagePackageTableView().getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
|
||||
}
|
||||
|
||||
return mMessagePackageViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with messages
|
||||
*/
|
||||
private TableView<MessagePackage> getMessagePackageTableView()
|
||||
{
|
||||
if(mMessagePackageTableView == null)
|
||||
{
|
||||
mMessagePackageTableView = new TableView<>();
|
||||
mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
|
||||
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
|
||||
mMessagePackageTableView.setItems(sortedList);
|
||||
|
||||
mMessagePackageTableView.setOnKeyPressed(event ->
|
||||
{
|
||||
if(KEY_CODE_COPY.match(event))
|
||||
{
|
||||
final Set<Integer> rows = new TreeSet<>();
|
||||
for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
|
||||
for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
|
||||
{
|
||||
rows.add(tablePosition.getRow());
|
||||
}
|
||||
@@ -282,7 +412,7 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
boolean firstCol = true;
|
||||
|
||||
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
|
||||
for (final TableColumn<?, ?> column : mMessagePackageTableView.getColumns())
|
||||
{
|
||||
if(firstCol)
|
||||
{
|
||||
@@ -313,23 +443,49 @@ public class P25P1Viewer extends VBox
|
||||
validColumn.setText("Valid");
|
||||
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
|
||||
|
||||
TableColumn timeslotColumn = new TableColumn();
|
||||
timeslotColumn.setPrefWidth(35);
|
||||
timeslotColumn.setText("TS");
|
||||
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
|
||||
|
||||
TableColumn messageColumn = new TableColumn();
|
||||
messageColumn.setPrefWidth(1000);
|
||||
messageColumn.setPrefWidth(900);
|
||||
messageColumn.setText("Message");
|
||||
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty();
|
||||
if(param.getValue() instanceof IMessage message)
|
||||
if(param.getValue() instanceof MessagePackage messagePackage)
|
||||
{
|
||||
property.set(message.toString());
|
||||
property.set(messagePackage.toString());
|
||||
}
|
||||
|
||||
return property;
|
||||
});
|
||||
|
||||
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
|
||||
TableColumn decodeEventCountColumn = new TableColumn();
|
||||
decodeEventCountColumn.setPrefWidth(50);
|
||||
decodeEventCountColumn.setText("Events");
|
||||
decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
|
||||
|
||||
TableColumn decoderStateEventCountColumn = new TableColumn();
|
||||
decoderStateEventCountColumn.setPrefWidth(50);
|
||||
decoderStateEventCountColumn.setText("States");
|
||||
decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
|
||||
|
||||
TableColumn channelStartCountColumn = new TableColumn();
|
||||
channelStartCountColumn.setPrefWidth(50);
|
||||
channelStartCountColumn.setText("Starts");
|
||||
channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
|
||||
|
||||
TableColumn audioSegmentCountColumn = new TableColumn();
|
||||
audioSegmentCountColumn.setPrefWidth(50);
|
||||
audioSegmentCountColumn.setText("Audio");
|
||||
audioSegmentCountColumn.setCellValueFactory(new PropertyValueFactory<>("audioSegmentCount"));
|
||||
|
||||
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
|
||||
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn, audioSegmentCountColumn);
|
||||
}
|
||||
|
||||
return mMessageTableView;
|
||||
return mMessagePackageTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,7 +499,7 @@ public class P25P1Viewer extends VBox
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Select DMR .bits Recording");
|
||||
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
{
|
||||
|
||||
740
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal file
740
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal file
@@ -0,0 +1,740 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 com.google.common.eventbus.EventBus;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
||||
import io.github.dsheirer.message.StuffBitsMessage;
|
||||
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25FrequencyBand;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageFramer;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageProcessor;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
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.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
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 2 viewer panel
|
||||
*/
|
||||
public class P25P2Viewer extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.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.p25p2";
|
||||
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.class);
|
||||
private Button mSelectFileButton;
|
||||
private Label mSelectedFileLabel;
|
||||
private TableView<MessagePackage> mMessagePackageTableView;
|
||||
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
|
||||
private FilteredList<MessagePackage> mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
|
||||
private CheckBox mShowTS0;
|
||||
private CheckBox mShowTS1;
|
||||
private CheckBox mShowTS2;
|
||||
private TextField mSearchText;
|
||||
private TextField mFindText;
|
||||
private Button mFindButton;
|
||||
private Button mFindNextButton;
|
||||
private ProgressIndicator mLoadingIndicator;
|
||||
private IntegerTextField mWACNTextField;
|
||||
private IntegerTextField mSystemTextField;
|
||||
private IntegerTextField mNACTextField;
|
||||
private Button mReloadButton;
|
||||
private StringProperty mLoadedFile = new SimpleStringProperty();
|
||||
private MessagePackageViewer mMessagePackageViewer;
|
||||
|
||||
public P25P2Viewer()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
setSpacing(5);
|
||||
|
||||
HBox fileBox = new HBox();
|
||||
fileBox.setMaxWidth(Double.MAX_VALUE);
|
||||
fileBox.setAlignment(Pos.CENTER_LEFT);
|
||||
fileBox.setSpacing(5);
|
||||
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
|
||||
|
||||
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
|
||||
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
|
||||
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
|
||||
|
||||
HBox scrambleSettingsBox = new HBox();
|
||||
scrambleSettingsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||
scrambleSettingsBox.setSpacing(5);
|
||||
Label wacnLabel = new Label("WACN:");
|
||||
Label systemLabel = new Label("SYSTEM:");
|
||||
Label nacLabel = new Label("NAC:");
|
||||
scrambleSettingsBox.getChildren().addAll(wacnLabel, getWACNTextField(), systemLabel, getSystemTextField(),
|
||||
nacLabel, getNACTextField(), getReloadButton());
|
||||
|
||||
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);
|
||||
|
||||
Label showLabel = new Label("Show:");
|
||||
HBox.setMargin(showLabel, new Insets(0,0,0,15));
|
||||
|
||||
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
|
||||
getSearchText(), showLabel, getShowTS0(), getShowTS1(), getShowTS2());
|
||||
|
||||
VBox.setVgrow(fileBox, Priority.NEVER);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
VBox.setVgrow(scrambleSettingsBox, Priority.NEVER);
|
||||
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
|
||||
|
||||
getChildren().addAll(fileBox, scrambleSettingsBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
|
||||
* the decoder states.
|
||||
* @param file name to parse
|
||||
* @return parsed frequency or zero.
|
||||
*/
|
||||
private static long getFrequencyFromFile(String file)
|
||||
{
|
||||
if(file == null || file.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(file.matches(FILE_FREQUENCY_REGEX))
|
||||
{
|
||||
Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
|
||||
Matcher m = p.matcher(file);
|
||||
if(m.find())
|
||||
{
|
||||
try
|
||||
{
|
||||
String raw = m.group(1);
|
||||
return Long.parseLong(raw);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't parse frequency from bits file [" + file + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 data.
|
||||
*/
|
||||
private void load(File file)
|
||||
{
|
||||
mLog.info("Loading File: " + file);
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
mLoadedFile.set(file.toString());
|
||||
mMessagePackages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
|
||||
int wacn = getWACNTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNACTextField().get();
|
||||
ScrambleParameters scrambleParameters = new ScrambleParameters(wacn, system, nac);
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
List<MessagePackage> messages = new ArrayList<>();
|
||||
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null);
|
||||
messageFramer.setScrambleParameters(scrambleParameters);
|
||||
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
||||
messageFramer.setListener(messageProcessor);
|
||||
Channel empty = new Channel("Empty");
|
||||
empty.setDecodeConfiguration(new DecodeConfigP25Phase2());
|
||||
|
||||
MessagePackager messagePackager = new MessagePackager();
|
||||
|
||||
//Setup a temporary event bus to capture channel start processing requests
|
||||
EventBus eventBus = new EventBus("debug");
|
||||
eventBus.register(messagePackager);
|
||||
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
|
||||
trafficChannelManager.setInterModuleEventBus(eventBus);
|
||||
|
||||
//Register to receive events
|
||||
trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
PatchGroupManager patchGroupManager = new PatchGroupManager();
|
||||
P25P2DecoderState decoderState1 = new P25P2DecoderState(empty, 1, trafficChannelManager,
|
||||
patchGroupManager);
|
||||
decoderState1.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
|
||||
decoderState1.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
P25P2DecoderState decoderState2 = new P25P2DecoderState(empty, 2, trafficChannelManager,
|
||||
patchGroupManager);
|
||||
decoderState2.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
|
||||
decoderState2.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
decoderState1.start();
|
||||
decoderState2.start();
|
||||
|
||||
long frequency = getFrequencyFromFile(mLoadedFile.get());
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
|
||||
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
|
||||
decoderState1.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 1));
|
||||
decoderState2.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 2));
|
||||
}
|
||||
|
||||
//TODO: testing use - PCWIN TDMA Frequency Band as preload data
|
||||
//ID:2 OFFSET:-45000000 SPACING:12500 BASE:851012500 TDMA BW:12500 TIMESLOTS:2 VOCODER:HALF_RATE
|
||||
P25FrequencyBand band = new P25FrequencyBand(2, 851012500l, -45000000l, 12500, 12500, 2);
|
||||
P25FrequencyBandPreloadDataContent content = new P25FrequencyBandPreloadDataContent(Collections.singleton(band));
|
||||
messageProcessor.preload(content);
|
||||
|
||||
messageProcessor.setMessageListener(message -> {
|
||||
if(!(message instanceof StuffBitsMessage))
|
||||
{
|
||||
// System.out.println(message);
|
||||
//Add the initial message to the packager so that it can be combined with any decoder state events.
|
||||
messagePackager.add(message);
|
||||
if(message.getTimeslot() == P25P1Message.TIMESLOT_1)
|
||||
{
|
||||
decoderState1.receive(message);
|
||||
}
|
||||
else if(message.getTimeslot() == P25P1Message.TIMESLOT_2)
|
||||
{
|
||||
decoderState2.receive(message);
|
||||
}
|
||||
|
||||
//Collect the packaged message with events
|
||||
messages.add(messagePackager.getMessageWithEvents());
|
||||
}
|
||||
});
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
// System.out.println("Processing Bytes " + buffer.capacity() + " / " + reader.getByteCounter());
|
||||
messageFramer.receive(buffer);
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getLoadingIndicator().setVisible(false);
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessagePackages.addAll(messages);
|
||||
getMessagePackageTableView().scrollTo(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("Can't load file: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the filter(s) applies to the list of messages
|
||||
*/
|
||||
private void updateFilters()
|
||||
{
|
||||
Predicate<MessagePackage> timeslotPredicate = message ->
|
||||
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
|
||||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
|
||||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
|
||||
|
||||
String filterText = getSearchText().getText();
|
||||
|
||||
if(filterText == null || filterText.isEmpty())
|
||||
{
|
||||
mFilteredMessagePackages.setPredicate(timeslotPredicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessagePackages.setPredicate(timeslotPredicate.and(textPredicate));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(MessagePackage messagePackage: mFilteredMessagePackages)
|
||||
{
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
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())
|
||||
{
|
||||
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected == null)
|
||||
{
|
||||
find(text);
|
||||
return;
|
||||
}
|
||||
|
||||
int row = mFilteredMessagePackages.indexOf(selected);
|
||||
|
||||
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
|
||||
{
|
||||
if(x < mFilteredMessagePackages.size())
|
||||
{
|
||||
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
|
||||
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MessagePackageViewer getMessagePackageViewer()
|
||||
{
|
||||
if(mMessagePackageViewer == null)
|
||||
{
|
||||
mMessagePackageViewer = new MessagePackageViewer();
|
||||
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
//Register for table selection events to display the selected value.
|
||||
getMessagePackageTableView().getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
|
||||
}
|
||||
|
||||
return mMessagePackageViewer;
|
||||
}
|
||||
|
||||
private IntegerTextField getWACNTextField()
|
||||
{
|
||||
if(mWACNTextField == null)
|
||||
{
|
||||
mWACNTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mWACNTextField;
|
||||
}
|
||||
|
||||
private IntegerTextField getSystemTextField()
|
||||
{
|
||||
if(mSystemTextField == null)
|
||||
{
|
||||
mSystemTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mSystemTextField;
|
||||
}
|
||||
|
||||
private IntegerTextField getNACTextField()
|
||||
{
|
||||
if(mNACTextField == null)
|
||||
{
|
||||
mNACTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mNACTextField;
|
||||
}
|
||||
|
||||
private Button getReloadButton()
|
||||
{
|
||||
if(mReloadButton == null)
|
||||
{
|
||||
mReloadButton = new Button("Reload");
|
||||
mReloadButton.disableProperty().bind(Bindings.isNull(mLoadedFile));
|
||||
mReloadButton.setOnAction(event -> load(new File(mLoadedFile.get())));
|
||||
}
|
||||
|
||||
return mReloadButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with message packages
|
||||
*/
|
||||
private TableView<MessagePackage> getMessagePackageTableView()
|
||||
{
|
||||
if(mMessagePackageTableView == null)
|
||||
{
|
||||
mMessagePackageTableView = new TableView<>();
|
||||
mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
|
||||
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
|
||||
mMessagePackageTableView.setItems(sortedList);
|
||||
|
||||
mMessagePackageTableView.setOnKeyPressed(event ->
|
||||
{
|
||||
if(KEY_CODE_COPY.match(event))
|
||||
{
|
||||
final Set<Integer> rows = new TreeSet<>();
|
||||
for (final TablePosition tablePosition : mMessagePackageTableView.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 : mMessagePackageTableView.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 timeslotColumn = new TableColumn();
|
||||
timeslotColumn.setPrefWidth(35);
|
||||
timeslotColumn.setText("TS");
|
||||
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
|
||||
|
||||
TableColumn messageColumn = new TableColumn();
|
||||
messageColumn.setPrefWidth(900);
|
||||
messageColumn.setText("Message");
|
||||
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty();
|
||||
if(param.getValue() instanceof MessagePackage messagePackage)
|
||||
{
|
||||
property.set(messagePackage.toString());
|
||||
}
|
||||
|
||||
return property;
|
||||
});
|
||||
|
||||
TableColumn decodeEventCountColumn = new TableColumn();
|
||||
decodeEventCountColumn.setPrefWidth(50);
|
||||
decodeEventCountColumn.setText("Events");
|
||||
decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
|
||||
|
||||
TableColumn decoderStateEventCountColumn = new TableColumn();
|
||||
decoderStateEventCountColumn.setPrefWidth(50);
|
||||
decoderStateEventCountColumn.setText("States");
|
||||
decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
|
||||
|
||||
TableColumn channelStartCountColumn = new TableColumn();
|
||||
channelStartCountColumn.setPrefWidth(50);
|
||||
channelStartCountColumn.setText("Starts");
|
||||
channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
|
||||
|
||||
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
|
||||
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
|
||||
}
|
||||
|
||||
return mMessagePackageTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 P25 Phase 2 .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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS0()
|
||||
{
|
||||
if(mShowTS0 == null)
|
||||
{
|
||||
mShowTS0 = new CheckBox("TS0");
|
||||
mShowTS0.setSelected(true);
|
||||
mShowTS0.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS1()
|
||||
{
|
||||
if(mShowTS1 == null)
|
||||
{
|
||||
mShowTS1 = new CheckBox("TS1");
|
||||
mShowTS1.setSelected(true);
|
||||
mShowTS1.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS2()
|
||||
{
|
||||
if(mShowTS2 == null)
|
||||
{
|
||||
mShowTS2 = new CheckBox("TS2");
|
||||
mShowTS2.setSelected(true);
|
||||
mShowTS2.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package io.github.dsheirer.identifier;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Identifier form. Indicates the type of identifier.
|
||||
*/
|
||||
@@ -64,4 +66,21 @@ public enum Form
|
||||
UNIQUE_ID,
|
||||
WACN,
|
||||
ANY;
|
||||
|
||||
Form()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity forms that are used to identify entities in P25 call events.
|
||||
*/
|
||||
public static EnumSet<Form> ENTITY_FORMS = EnumSet.of(Form.RADIO, Form.TALKGROUP, Form.PATCH_GROUP, Form.TELEPHONE_NUMBER);
|
||||
|
||||
/**
|
||||
* Indicates if this form is a form that can be used to identify an entity
|
||||
*/
|
||||
public boolean isEntityForm()
|
||||
{
|
||||
return ENTITY_FORMS.contains(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -32,6 +32,12 @@ public enum IdentifierClass
|
||||
//Communication network identifier
|
||||
NETWORK,
|
||||
|
||||
//User network address
|
||||
USER_NETWORK_ADDRESS,
|
||||
|
||||
//User network port
|
||||
USER_NETWORK_PORT,
|
||||
|
||||
//Communication user identifier
|
||||
USER;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,14 +20,13 @@
|
||||
package io.github.dsheirer.identifier;
|
||||
|
||||
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* (Immutable) Collection of identifiers with convenient accessor methods
|
||||
@@ -274,49 +273,105 @@ public class IdentifierCollection
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first identifier in this collection that is assigned a FROM role
|
||||
* Indicates if this collection has an identifier that matches the specified identiifer.
|
||||
* @param toCheck identifier
|
||||
* @return true if the identifier already exists in the collection.
|
||||
*/
|
||||
public boolean hasIdentifier(Identifier toCheck)
|
||||
{
|
||||
if(toCheck == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(Identifier identifier : mIdentifiers)
|
||||
{
|
||||
if(identifier.equals(toCheck))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first entity identifier in this collection that is assigned a FROM role
|
||||
*/
|
||||
public Identifier getFromIdentifier()
|
||||
{
|
||||
Identifier from = getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(from == null)
|
||||
if(from != null)
|
||||
{
|
||||
List<Identifier> fromIdentifiers = getIdentifiers(Role.FROM);
|
||||
|
||||
if(!fromIdentifiers.isEmpty())
|
||||
{
|
||||
from = fromIdentifiers.get(0);
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
return from;
|
||||
List<Identifier> identifiers = getIdentifiers(Form.TELEPHONE_NUMBER);
|
||||
|
||||
if(!identifiers.isEmpty())
|
||||
{
|
||||
return identifiers.get(0);
|
||||
}
|
||||
|
||||
List<Identifier> fromIdentifiers = getIdentifiers(Role.FROM);
|
||||
|
||||
if(!fromIdentifiers.isEmpty())
|
||||
{
|
||||
return fromIdentifiers.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first identifier in this collection that is assigned a TO role
|
||||
* Returns the first entity identifier in this collection that is assigned a TO role
|
||||
*/
|
||||
public Identifier getToIdentifier()
|
||||
{
|
||||
Identifier to = getIdentifier(IdentifierClass.USER, Form.PATCH_GROUP, Role.TO);
|
||||
|
||||
if(to == null)
|
||||
if(to != null)
|
||||
{
|
||||
to = getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
|
||||
return to;
|
||||
}
|
||||
|
||||
if(to == null)
|
||||
to = getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
List<Identifier> toIdentifiers = getIdentifiers(Role.TO);
|
||||
if(!toIdentifiers.isEmpty())
|
||||
return to;
|
||||
}
|
||||
|
||||
to = getIdentifier(IdentifierClass.USER, Form.RADIO, Role.TO);
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
return to;
|
||||
}
|
||||
|
||||
List<Identifier> toIdentifiers = getIdentifiers(Role.TO);
|
||||
|
||||
for(Identifier identifier: toIdentifiers)
|
||||
{
|
||||
if(identifier.getForm() != Form.ENCRYPTION_KEY)
|
||||
{
|
||||
to = toIdentifiers.get(0);
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
|
||||
return to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an encryption key identiier or null.
|
||||
* @return key or null.
|
||||
*/
|
||||
public Identifier getEncryptionIdentifier()
|
||||
{
|
||||
return getIdentifier(IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier;
|
||||
|
||||
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Identifier collection with methods for changing or updating managed identifiers
|
||||
@@ -209,6 +207,15 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
||||
remove(existing);
|
||||
add(identifier);
|
||||
}
|
||||
//Always replace a radio identifier with a fully qualified variant of itself
|
||||
else if(existing instanceof RadioIdentifier &&
|
||||
!(existing instanceof FullyQualifiedRadioIdentifier) &&
|
||||
identifier instanceof FullyQualifiedRadioIdentifier &&
|
||||
existing.getValue().equals(identifier.getValue()))
|
||||
{
|
||||
remove(existing);
|
||||
add(identifier);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Talker alias cache manager. Collects observed talker aliases and inserts them into an identifier collection when
|
||||
* the corresponding radio is active in a FROM role.
|
||||
*
|
||||
* This implementation is thread safe and is intended to be used across control and traffic channels.
|
||||
*/
|
||||
public class TalkerAliasManager
|
||||
{
|
||||
private Map<Integer,TalkerAliasIdentifier> mAliasMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public TalkerAliasManager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the alias for the
|
||||
* @param identifier
|
||||
* @param alias
|
||||
*/
|
||||
public void update(RadioIdentifier identifier, TalkerAliasIdentifier alias)
|
||||
{
|
||||
if(identifier.getRole() == Role.FROM)
|
||||
{
|
||||
mAliasMap.put(identifier.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an alias exists for the identifier
|
||||
* @param radioIdentifier to test
|
||||
* @return true if an alias exists.
|
||||
*/
|
||||
public boolean hasAlias(RadioIdentifier radioIdentifier)
|
||||
{
|
||||
return mAliasMap.containsKey(radioIdentifier.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the immutable identifier collection by detecting a radio identifier with the FROM role, lookup a
|
||||
* matching alias, and insert the alias into a new mutable identifier collection.
|
||||
* @param originalIC to enrich
|
||||
* @return enriched identifier collection or the original identifier collection if we don't have an alias.
|
||||
*/
|
||||
public synchronized IdentifierCollection enrich(IdentifierCollection originalIC)
|
||||
{
|
||||
Identifier fromRadio = originalIC.getFromIdentifier();
|
||||
|
||||
if(fromRadio instanceof RadioIdentifier rid)
|
||||
{
|
||||
TalkerAliasIdentifier alias = mAliasMap.get(rid.getValue());
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
MutableIdentifierCollection enrichedIC = new MutableIdentifierCollection(originalIC.getIdentifiers());
|
||||
enrichedIC.update(alias);
|
||||
return enrichedIC;
|
||||
}
|
||||
}
|
||||
|
||||
return originalIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the mutable identifier collection by detecting a radio identifier with the FROM role, lookup a
|
||||
* matching alias, and insert the alias into the mutable identifier collection argument.
|
||||
* @param mic to enrich
|
||||
*/
|
||||
public synchronized void enrichMutable(MutableIdentifierCollection mic)
|
||||
{
|
||||
Identifier fromRadio = mic.getFromIdentifier();
|
||||
|
||||
if(fromRadio instanceof RadioIdentifier rid)
|
||||
{
|
||||
TalkerAliasIdentifier alias = mAliasMap.get(rid.getValue());
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
mic.update(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a summary listing of talker aliases
|
||||
* @return summary.
|
||||
*/
|
||||
public synchronized String getAliasSummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Active System Radio Aliases\n");
|
||||
List<Integer> radios = new ArrayList<>(mAliasMap.keySet());
|
||||
|
||||
if(radios.size() > 0)
|
||||
{
|
||||
Collections.sort(radios);
|
||||
for(Integer radio : radios)
|
||||
{
|
||||
sb.append(" ").append(radio);
|
||||
sb.append("\t").append(mAliasMap.get(radio).getValue());
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" None\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ public abstract class IntegerIdentifier extends Identifier<Integer>
|
||||
super(value, identifierClass, form, role);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.ipv4;
|
||||
@@ -29,6 +28,6 @@ public abstract class IPV4Identifier extends Identifier<IPV4Address>
|
||||
{
|
||||
public IPV4Identifier(IPV4Address value, Role role)
|
||||
{
|
||||
super(value, IdentifierClass.USER, Form.IPV4_ADDRESS, role);
|
||||
super(value, IdentifierClass.USER_NETWORK_ADDRESS, Form.IPV4_ADDRESS, role);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user