mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Compare commits
68 Commits
v0.6.0-bet
...
v0.6.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0492f19a95 | ||
|
|
098b039154 | ||
|
|
5c6e6059f8 | ||
|
|
72d45543df | ||
|
|
e96fc46315 | ||
|
|
f0cfbb80dd | ||
|
|
a199f03a60 | ||
|
|
526c9609ef | ||
|
|
ec9ceeefb3 | ||
|
|
b6c6505824 | ||
|
|
93c61c48f9 | ||
|
|
9b89e80bb5 | ||
|
|
e0ecaa6e08 | ||
|
|
a7c89b5d1a | ||
|
|
834846d339 | ||
|
|
45c8bb73fa | ||
|
|
d2580f8a99 | ||
|
|
804c2e6359 | ||
|
|
3f2ec612d5 | ||
|
|
8bead998f5 | ||
|
|
2ecdaba183 | ||
|
|
82cb10a03d | ||
|
|
8717ce38eb | ||
|
|
5858a85a50 | ||
|
|
65d16da997 | ||
|
|
b1b5ae5fd2 | ||
|
|
b5ec6d8877 | ||
|
|
8599aebed9 | ||
|
|
2b9e753033 | ||
|
|
dba50744a9 | ||
|
|
490569e023 | ||
|
|
e79f2af759 | ||
|
|
ec7ee1ce91 | ||
|
|
672207c27c | ||
|
|
a1a6b3e988 | ||
|
|
2347135614 | ||
|
|
93438ddd97 | ||
|
|
ae31c41643 |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -30,7 +30,11 @@ typedef void *HANDLE;
|
||||
..\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
|
||||
Windows:
|
||||
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 -t io.github.dsheirer.source.tuner.sdrplay.api.v3_14
|
||||
|
||||
5. Do this for all versions of the API.
|
||||
Linux:
|
||||
~/Downloads/jextract-20/bin/jextract ~/IdeaProjects/sdrtrunk/artifacts/sdrplay-api-headers/v3_14/sdrplay_api.h -l libsdrplay_api --source -t io.github.dsheirer.source.tuner.sdrplay.api.v3_14
|
||||
|
||||
5. Compare the 3.07 header files to the newest version header files. Delete any auto-generated files that have not
|
||||
changed and then update the factory class to construct variation classes where there are differences.
|
||||
|
||||
234
artifacts/sdrplay-api-headers/v3_14/sdrplay_api.h
Normal file
234
artifacts/sdrplay-api-headers/v3_14/sdrplay_api.h
Normal file
@@ -0,0 +1,234 @@
|
||||
#ifndef SDRPLAY_API_H
|
||||
#define SDRPLAY_API_H
|
||||
|
||||
#include "sdrplay_api_dev.h"
|
||||
#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__)
|
||||
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
|
||||
|
||||
// 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.14)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
#define SDRPLAY_MAX_TUNERS_PER_DEVICE (2)
|
||||
|
||||
#define SDRPLAY_MAX_SER_NO_LEN (64)
|
||||
#define SDRPLAY_MAX_ROOT_NM_LEN (32)
|
||||
|
||||
#define SDRPLAY_RSP1_ID (1)
|
||||
#define SDRPLAY_RSP1A_ID (255)
|
||||
#define SDRPLAY_RSP2_ID (2)
|
||||
#define SDRPLAY_RSPduo_ID (3)
|
||||
#define SDRPLAY_RSPdx_ID (4)
|
||||
#define SDRPLAY_RSP1B_ID (6)
|
||||
|
||||
// Enum types
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Success = 0,
|
||||
sdrplay_api_Fail = 1,
|
||||
sdrplay_api_InvalidParam = 2,
|
||||
sdrplay_api_OutOfRange = 3,
|
||||
sdrplay_api_GainUpdateError = 4,
|
||||
sdrplay_api_RfUpdateError = 5,
|
||||
sdrplay_api_FsUpdateError = 6,
|
||||
sdrplay_api_HwError = 7,
|
||||
sdrplay_api_AliasingError = 8,
|
||||
sdrplay_api_AlreadyInitialised = 9,
|
||||
sdrplay_api_NotInitialised = 10,
|
||||
sdrplay_api_NotEnabled = 11,
|
||||
sdrplay_api_HwVerError = 12,
|
||||
sdrplay_api_OutOfMemError = 13,
|
||||
sdrplay_api_ServiceNotResponding = 14,
|
||||
sdrplay_api_StartPending = 15,
|
||||
sdrplay_api_StopPending = 16,
|
||||
sdrplay_api_InvalidMode = 17,
|
||||
sdrplay_api_FailedVerification1 = 18,
|
||||
sdrplay_api_FailedVerification2 = 19,
|
||||
sdrplay_api_FailedVerification3 = 20,
|
||||
sdrplay_api_FailedVerification4 = 21,
|
||||
sdrplay_api_FailedVerification5 = 22,
|
||||
sdrplay_api_FailedVerification6 = 23,
|
||||
sdrplay_api_InvalidServiceVersion = 24
|
||||
} sdrplay_api_ErrT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_Dev_Fs = 0x00000001,
|
||||
sdrplay_api_Update_Dev_Ppm = 0x00000002,
|
||||
sdrplay_api_Update_Dev_SyncUpdate = 0x00000004,
|
||||
sdrplay_api_Update_Dev_ResetFlags = 0x00000008,
|
||||
|
||||
sdrplay_api_Update_Rsp1a_BiasTControl = 0x00000010,
|
||||
sdrplay_api_Update_Rsp1a_RfNotchControl = 0x00000020,
|
||||
sdrplay_api_Update_Rsp1a_RfDabNotchControl = 0x00000040,
|
||||
|
||||
sdrplay_api_Update_Rsp2_BiasTControl = 0x00000080,
|
||||
sdrplay_api_Update_Rsp2_AmPortSelect = 0x00000100,
|
||||
sdrplay_api_Update_Rsp2_AntennaControl = 0x00000200,
|
||||
sdrplay_api_Update_Rsp2_RfNotchControl = 0x00000400,
|
||||
sdrplay_api_Update_Rsp2_ExtRefControl = 0x00000800,
|
||||
|
||||
sdrplay_api_Update_RspDuo_ExtRefControl = 0x00001000,
|
||||
|
||||
sdrplay_api_Update_Master_Spare_1 = 0x00002000,
|
||||
sdrplay_api_Update_Master_Spare_2 = 0x00004000,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
// Note: sdrplay_api_Update_Tuner_Gr MUST be the first value defined in this section!
|
||||
sdrplay_api_Update_Tuner_Gr = 0x00008000,
|
||||
sdrplay_api_Update_Tuner_GrLimits = 0x00010000,
|
||||
sdrplay_api_Update_Tuner_Frf = 0x00020000,
|
||||
sdrplay_api_Update_Tuner_BwType = 0x00040000,
|
||||
sdrplay_api_Update_Tuner_IfType = 0x00080000,
|
||||
sdrplay_api_Update_Tuner_DcOffset = 0x00100000,
|
||||
sdrplay_api_Update_Tuner_LoMode = 0x00200000,
|
||||
|
||||
sdrplay_api_Update_Ctrl_DCoffsetIQimbalance = 0x00400000,
|
||||
sdrplay_api_Update_Ctrl_Decimation = 0x00800000,
|
||||
sdrplay_api_Update_Ctrl_Agc = 0x01000000,
|
||||
sdrplay_api_Update_Ctrl_AdsbMode = 0x02000000,
|
||||
sdrplay_api_Update_Ctrl_OverloadMsgAck = 0x04000000,
|
||||
|
||||
sdrplay_api_Update_RspDuo_BiasTControl = 0x08000000,
|
||||
sdrplay_api_Update_RspDuo_AmPortSelect = 0x10000000,
|
||||
sdrplay_api_Update_RspDuo_Tuner1AmNotchControl = 0x20000000,
|
||||
sdrplay_api_Update_RspDuo_RfNotchControl = 0x40000000,
|
||||
sdrplay_api_Update_RspDuo_RfDabNotchControl = 0x80000000,
|
||||
} sdrplay_api_ReasonForUpdateT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_Ext1_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_RspDx_HdrEnable = 0x00000001,
|
||||
sdrplay_api_Update_RspDx_BiasTControl = 0x00000002,
|
||||
sdrplay_api_Update_RspDx_AntennaControl = 0x00000004,
|
||||
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;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_DbgLvl_Disable = 0,
|
||||
sdrplay_api_DbgLvl_Verbose = 1,
|
||||
sdrplay_api_DbgLvl_Warning = 2,
|
||||
sdrplay_api_DbgLvl_Error = 3,
|
||||
sdrplay_api_DbgLvl_Message = 4,
|
||||
} sdrplay_api_DbgLvl_t;
|
||||
|
||||
// Device structure
|
||||
typedef struct
|
||||
{
|
||||
char SerNo[SDRPLAY_MAX_SER_NO_LEN];
|
||||
unsigned char hwVer;
|
||||
sdrplay_api_TunerSelectT tuner;
|
||||
sdrplay_api_RspDuoModeT rspDuoMode;
|
||||
unsigned char valid;
|
||||
double rspDuoSampleFreq;
|
||||
HANDLE dev;
|
||||
} sdrplay_api_DeviceT;
|
||||
|
||||
// Device parameter structure
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DevParamsT *devParams;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelA;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelB;
|
||||
} sdrplay_api_DeviceParamsT;
|
||||
|
||||
// Extended error message structure
|
||||
typedef struct
|
||||
{
|
||||
char file[256];
|
||||
char function[256];
|
||||
int line;
|
||||
char message[1024];
|
||||
} sdrplay_api_ErrorInfoT;
|
||||
|
||||
// Comman API function types
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Open_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Close_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ApiVersion_t)(float *apiVer);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_LockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_UnlockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDevices_t)(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_DeviceT *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
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DebugEnable_t)(HANDLE dev, sdrplay_api_DbgLvl_t dbgLvl);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDeviceParams_t)(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Init_t)(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Uninit_t)(HANDLE dev);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Update_t)(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoActiveTuner_t)(HANDLE dev, sdrplay_api_TunerSelectT *tuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoDualTunerModeSampleRate_t)(double *currentSampleRate, double newSampleRate);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoMode_t)(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);
|
||||
|
||||
// API function definitions
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
_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_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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //SDRPLAY_API_H
|
||||
79
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_callback.h
Normal file
79
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_callback.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#ifndef SDRPLAY_API_CALLBACK_H
|
||||
#define SDRPLAY_API_CALLBACK_H
|
||||
|
||||
// Event callback enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Overload_Detected = 0,
|
||||
sdrplay_api_Overload_Corrected = 1,
|
||||
} sdrplay_api_PowerOverloadCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_MasterInitialised = 0,
|
||||
sdrplay_api_SlaveAttached = 1,
|
||||
sdrplay_api_SlaveDetached = 2,
|
||||
sdrplay_api_SlaveInitialised = 3,
|
||||
sdrplay_api_SlaveUninitialised = 4,
|
||||
sdrplay_api_MasterDllDisappeared = 5,
|
||||
sdrplay_api_SlaveDllDisappeared = 6,
|
||||
} sdrplay_api_RspDuoModeCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_GainChange = 0,
|
||||
sdrplay_api_PowerOverloadChange = 1,
|
||||
sdrplay_api_DeviceRemoved = 2,
|
||||
sdrplay_api_RspDuoModeChange = 3,
|
||||
sdrplay_api_DeviceFailure = 4,
|
||||
} sdrplay_api_EventT;
|
||||
|
||||
// Event callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int gRdB;
|
||||
unsigned int lnaGRdB;
|
||||
double currGain;
|
||||
} sdrplay_api_GainCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_PowerOverloadCbEventIdT powerOverloadChangeType;
|
||||
} sdrplay_api_PowerOverloadCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDuoModeCbEventIdT modeChangeType;
|
||||
} sdrplay_api_RspDuoModeCbParamT;
|
||||
|
||||
// Event parameters overlay
|
||||
typedef union
|
||||
{
|
||||
sdrplay_api_GainCbParamT gainParams;
|
||||
sdrplay_api_PowerOverloadCbParamT powerOverloadParams;
|
||||
sdrplay_api_RspDuoModeCbParamT rspDuoModeParams;
|
||||
} sdrplay_api_EventParamsT;
|
||||
|
||||
// Stream callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int firstSampleNum;
|
||||
int grChanged;
|
||||
int rfChanged;
|
||||
int fsChanged;
|
||||
unsigned int numSamples;
|
||||
} sdrplay_api_StreamCbParamsT;
|
||||
|
||||
// Callback function prototypes
|
||||
typedef void (*sdrplay_api_StreamCallback_t)(short *xi, short *xq, sdrplay_api_StreamCbParamsT *params, unsigned int numSamples, unsigned int reset, void *cbContext);
|
||||
typedef void (*sdrplay_api_EventCallback_t)(sdrplay_api_EventT eventId, sdrplay_api_TunerSelectT tuner, sdrplay_api_EventParamsT *params, void *cbContext);
|
||||
|
||||
// Callback function struct
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_StreamCallback_t StreamACbFn;
|
||||
sdrplay_api_StreamCallback_t StreamBCbFn;
|
||||
sdrplay_api_EventCallback_t EventCbFn;
|
||||
} sdrplay_api_CallbackFnsT;
|
||||
|
||||
#endif //SDRPLAY_API_CALLBACK_H
|
||||
55
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_control.h
Normal file
55
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_control.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef SDRPLAY_API_CONTROL_H
|
||||
#define SDRPLAY_API_CONTROL_H
|
||||
|
||||
// Control parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_AGC_DISABLE = 0,
|
||||
sdrplay_api_AGC_100HZ = 1,
|
||||
sdrplay_api_AGC_50HZ = 2,
|
||||
sdrplay_api_AGC_5HZ = 3,
|
||||
sdrplay_api_AGC_CTRL_EN = 4
|
||||
} sdrplay_api_AgcControlT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ADSB_DECIMATION = 0,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_LOWPASS = 1,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_2MHZ = 2,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_3MHZ = 3
|
||||
} sdrplay_api_AdsbModeT;
|
||||
|
||||
// Control parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char DCenable; // default: 1
|
||||
unsigned char IQenable; // default: 1
|
||||
} sdrplay_api_DcOffsetT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char enable; // default: 0
|
||||
unsigned char decimationFactor; // default: 1
|
||||
unsigned char wideBandSignal; // default: 0
|
||||
} sdrplay_api_DecimationT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_AgcControlT enable; // default: sdrplay_api_AGC_50HZ
|
||||
int setPoint_dBfs; // default: -60
|
||||
unsigned short attack_ms; // default: 0
|
||||
unsigned short decay_ms; // default: 0
|
||||
unsigned short decay_delay_ms; // default: 0
|
||||
unsigned short decay_threshold_dB; // default: 0
|
||||
int syncUpdate; // default: 0
|
||||
} sdrplay_api_AgcT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DcOffsetT dcOffset;
|
||||
sdrplay_api_DecimationT decimation;
|
||||
sdrplay_api_AgcT agc;
|
||||
sdrplay_api_AdsbModeT adsbMode; //default: sdrplay_api_ADSB_DECIMATION
|
||||
} sdrplay_api_ControlParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_CONTROL_H
|
||||
51
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_dev.h
Normal file
51
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_dev.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef SDRPLAY_API_DEV_H
|
||||
#define SDRPLAY_API_DEV_H
|
||||
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
// Dev parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ISOCH = 0,
|
||||
sdrplay_api_BULK = 1
|
||||
} sdrplay_api_TransferModeT;
|
||||
|
||||
// Dev parameter structs
|
||||
typedef struct
|
||||
{
|
||||
double fsHz; // default: 2000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
unsigned char reCal; // default: 0
|
||||
} sdrplay_api_FsFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned int sampleNum; // default: 0
|
||||
unsigned int period; // default: 0
|
||||
} sdrplay_api_SyncUpdateT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
unsigned char resetFsUpdate; // default: 0
|
||||
} sdrplay_api_ResetFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double ppm; // default: 0.0
|
||||
sdrplay_api_FsFreqT fsFreq;
|
||||
sdrplay_api_SyncUpdateT syncUpdate;
|
||||
sdrplay_api_ResetFlagsT resetFlags;
|
||||
sdrplay_api_TransferModeT mode; // default: sdrplay_api_ISOCH
|
||||
unsigned int samplesPerPkt; // default: 0 (output param)
|
||||
sdrplay_api_Rsp1aParamsT rsp1aParams;
|
||||
sdrplay_api_Rsp2ParamsT rsp2Params;
|
||||
sdrplay_api_RspDuoParamsT rspDuoParams;
|
||||
sdrplay_api_RspDxParamsT rspDxParams;
|
||||
} sdrplay_api_DevParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_DEV_H
|
||||
22
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rsp1a.h
Normal file
22
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rsp1a.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef SDRPLAY_API_RSP1A_H
|
||||
#define SDRPLAY_API_RSP1A_H
|
||||
|
||||
#define RSPIA_NUM_LNA_STATES 10
|
||||
#define RSPIA_NUM_LNA_STATES_AM 7
|
||||
#define RSPIA_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSP1A parameter enums
|
||||
|
||||
// RSP1A parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP1A_H
|
||||
35
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rsp2.h
Normal file
35
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rsp2.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef SDRPLAY_API_RSP2_H
|
||||
#define SDRPLAY_API_RSP2_H
|
||||
|
||||
#define RSPII_NUM_LNA_STATES 9
|
||||
#define RSPII_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPII_NUM_LNA_STATES_420MHZ 6
|
||||
|
||||
// RSP2 parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_ANTENNA_A = 5,
|
||||
sdrplay_api_Rsp2_ANTENNA_B = 6,
|
||||
} sdrplay_api_Rsp2_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_AMPORT_1 = 1,
|
||||
sdrplay_api_Rsp2_AMPORT_2 = 0,
|
||||
} sdrplay_api_Rsp2_AmPortSelectT;
|
||||
|
||||
// RSP2 parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char extRefOutputEn; // default: 0
|
||||
} sdrplay_api_Rsp2ParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_Rsp2_AmPortSelectT amPortSel; // default: sdrplay_api_Rsp2_AMPORT_2
|
||||
sdrplay_api_Rsp2_AntennaSelectT antennaSel; // default: sdrplay_api_Rsp2_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp2TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP2_H
|
||||
47
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rspDuo.h
Normal file
47
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rspDuo.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef SDRPLAY_API_RSPduo_H
|
||||
#define SDRPLAY_API_RSPduo_H
|
||||
|
||||
#define RSPDUO_NUM_LNA_STATES 10
|
||||
#define RSPDUO_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPDUO_NUM_LNA_STATES_AM 7
|
||||
#define RSPDUO_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSPduo parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuoMode_Unknown = 0,
|
||||
sdrplay_api_RspDuoMode_Single_Tuner = 1,
|
||||
sdrplay_api_RspDuoMode_Dual_Tuner = 2,
|
||||
sdrplay_api_RspDuoMode_Master = 4,
|
||||
sdrplay_api_RspDuoMode_Slave = 8,
|
||||
} sdrplay_api_RspDuoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuo_AMPORT_1 = 1,
|
||||
sdrplay_api_RspDuo_AMPORT_2 = 0,
|
||||
} sdrplay_api_RspDuo_AmPortSelectT;
|
||||
|
||||
// RSPduo parameter structs
|
||||
typedef struct
|
||||
{
|
||||
int extRefOutputEn; // default: 0
|
||||
} 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
|
||||
46
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rspDx.h
Normal file
46
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rspDx.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef SDRPLAY_API_RSPDX_H
|
||||
#define SDRPLAY_API_RSPDX_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
|
||||
#define RSPDX_NUM_LNA_STATES 28 // Number of LNA states in all bands (except where defined differently below)
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_0_12 19 // Number of LNA states when using AM Port 2 between 0 and 12MHz
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_12_50 20 // Number of LNA states when using AM Port 2 between 12 and 50MHz
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_50_60 25 // Number of LNA states when using AM Port 2 between 50 and 60MHz
|
||||
#define RSPDX_NUM_LNA_STATES_VHF_BAND3 27 // Number of LNA states in VHF and Band3
|
||||
#define RSPDX_NUM_LNA_STATES_420MHZ 21 // Number of LNA states in 420MHz band
|
||||
#define RSPDX_NUM_LNA_STATES_LBAND 19 // Number of LNA states in L-band
|
||||
#define RSPDX_NUM_LNA_STATES_DX 22 // Number of LNA states in DX path
|
||||
|
||||
// RSPdx parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_ANTENNA_A = 0,
|
||||
sdrplay_api_RspDx_ANTENNA_B = 1,
|
||||
sdrplay_api_RspDx_ANTENNA_C = 2,
|
||||
} sdrplay_api_RspDx_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_200 = 0,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_500 = 1,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_200 = 2,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_700 = 3,
|
||||
} sdrplay_api_RspDx_HdrModeBwT;
|
||||
|
||||
// RSPdx parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char hdrEnable; // default: 0
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDx_AntennaSelectT antennaSel; // default: sdrplay_api_RspDx_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_RspDxParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDx_HdrModeBwT hdrBw; // default: sdrplay_api_RspDx_HDRMODE_BW_1_700
|
||||
} sdrplay_api_RspDxTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPDX_H
|
||||
21
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rx_channel.h
Normal file
21
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_rx_channel.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef SDRPLAY_API_RX_CHANNEL_H
|
||||
#define SDRPLAY_API_RX_CHANNEL_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
#include "sdrplay_api_control.h"
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_TunerParamsT tunerParams;
|
||||
sdrplay_api_ControlParamsT ctrlParams;
|
||||
sdrplay_api_Rsp1aTunerParamsT rsp1aTunerParams;
|
||||
sdrplay_api_Rsp2TunerParamsT rsp2TunerParams;
|
||||
sdrplay_api_RspDuoTunerParamsT rspDuoTunerParams;
|
||||
sdrplay_api_RspDxTunerParamsT rspDxTunerParams;
|
||||
} sdrplay_api_RxChannelParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RX_CHANNEL_H
|
||||
93
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_tuner.h
Normal file
93
artifacts/sdrplay-api-headers/v3_14/sdrplay_api_tuner.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef SDRPLAY_API_TUNER_H
|
||||
#define SDRPLAY_API_TUNER_H
|
||||
|
||||
#define MAX_BB_GR (59)
|
||||
|
||||
// Tuner parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_BW_Undefined = 0,
|
||||
sdrplay_api_BW_0_200 = 200,
|
||||
sdrplay_api_BW_0_300 = 300,
|
||||
sdrplay_api_BW_0_600 = 600,
|
||||
sdrplay_api_BW_1_536 = 1536,
|
||||
sdrplay_api_BW_5_000 = 5000,
|
||||
sdrplay_api_BW_6_000 = 6000,
|
||||
sdrplay_api_BW_7_000 = 7000,
|
||||
sdrplay_api_BW_8_000 = 8000
|
||||
} sdrplay_api_Bw_MHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_IF_Undefined = -1,
|
||||
sdrplay_api_IF_Zero = 0,
|
||||
sdrplay_api_IF_0_450 = 450,
|
||||
sdrplay_api_IF_1_620 = 1620,
|
||||
sdrplay_api_IF_2_048 = 2048
|
||||
} sdrplay_api_If_kHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_LO_Undefined = 0,
|
||||
sdrplay_api_LO_Auto = 1,
|
||||
sdrplay_api_LO_120MHz = 2,
|
||||
sdrplay_api_LO_144MHz = 3,
|
||||
sdrplay_api_LO_168MHz = 4
|
||||
} sdrplay_api_LoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_EXTENDED_MIN_GR = 0,
|
||||
sdrplay_api_NORMAL_MIN_GR = 20
|
||||
} sdrplay_api_MinGainReductionT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Tuner_Neither = 0,
|
||||
sdrplay_api_Tuner_A = 1,
|
||||
sdrplay_api_Tuner_B = 2,
|
||||
sdrplay_api_Tuner_Both = 3,
|
||||
} sdrplay_api_TunerSelectT;
|
||||
|
||||
// Tuner parameter structs
|
||||
typedef struct
|
||||
{
|
||||
float curr;
|
||||
float max;
|
||||
float min;
|
||||
} sdrplay_api_GainValuesT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int gRdB; // default: 50
|
||||
unsigned char LNAstate; // default: 0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
sdrplay_api_MinGainReductionT minGr; // default: sdrplay_api_NORMAL_MIN_GR
|
||||
sdrplay_api_GainValuesT gainVals; // output parameter
|
||||
} sdrplay_api_GainT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double rfHz; // default: 200000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
} sdrplay_api_RfFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char dcCal; // default: 3 (Periodic mode)
|
||||
unsigned char speedUp; // default: 0 (No speedup)
|
||||
int trackTime; // default: 1 (=> time in uSec = (72 * 3 * trackTime) / 24e6 = 9uSec)
|
||||
int refreshRateTime; // default: 2048 (=> time in uSec = (72 * 3 * refreshRateTime) / 24e6 = 18432uSec)
|
||||
} sdrplay_api_DcOffsetTunerT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_Bw_MHzT bwType; // default: sdrplay_api_BW_0_200
|
||||
sdrplay_api_If_kHzT ifType; // default: sdrplay_api_IF_Zero
|
||||
sdrplay_api_LoModeT loMode; // default: sdrplay_api_LO_Auto
|
||||
sdrplay_api_GainT gain;
|
||||
sdrplay_api_RfFreqT rfFreq;
|
||||
sdrplay_api_DcOffsetTunerT dcOffsetTuner;
|
||||
} sdrplay_api_TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_TUNER_H
|
||||
10
build.gradle
10
build.gradle
@@ -211,8 +211,8 @@ def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jd
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||
|
||||
/**
|
||||
@@ -229,7 +229,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||
//jdk.accessibility is used with assistive technologies like screen readers
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility')
|
||||
//java.management for JVM resource monitoring
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
@@ -340,7 +341,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||
}
|
||||
}
|
||||
//Linux x86-64
|
||||
//OSX x86-64
|
||||
if(file(jdk_osx_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||
}
|
||||
@@ -352,4 +353,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
|
||||
configure(rt, jvmArgsLinux);
|
||||
}
|
||||
|
||||
|
||||
@@ -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-beta-5
|
||||
projectVersion=0.6.1-beta-1
|
||||
@@ -30,11 +30,13 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -77,6 +79,18 @@ public class AliasFactory
|
||||
Min copyMin = new Min();
|
||||
copyMin.setMin(originalMin.getMin());
|
||||
return copyMin;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
|
||||
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
|
||||
originalP25.getSystem(), originalP25.getValue());
|
||||
copyP25.setOverlap(originalP25.overlapProperty().get());
|
||||
return copyP25;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
|
||||
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
|
||||
originalFqt.getSystem(), originalFqt.getValue());
|
||||
copyFqt.setOverlap(originalFqt.overlapProperty().get());
|
||||
return copyFqt;
|
||||
case PRIORITY:
|
||||
Priority originalPriority = (Priority)id;
|
||||
Priority copyPriority = new Priority();
|
||||
|
||||
@@ -24,10 +24,12 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -37,9 +39,11 @@ import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
@@ -155,6 +159,32 @@ public class AliasList
|
||||
|
||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio qualifiedRadio = (P25FullyQualifiedRadio) id;
|
||||
|
||||
RadioAliasList p25RadioAliasList = mRadioProtocolMap.get(qualifiedRadio.getProtocol());
|
||||
|
||||
if(p25RadioAliasList == null)
|
||||
{
|
||||
p25RadioAliasList = new RadioAliasList();
|
||||
mRadioProtocolMap.put(qualifiedRadio.getProtocol(), p25RadioAliasList);
|
||||
}
|
||||
|
||||
p25RadioAliasList.add(qualifiedRadio, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup qualifiedTalkgroup = (P25FullyQualifiedTalkgroup) id;
|
||||
|
||||
TalkgroupAliasList p25TalkgroupAliasList = mTalkgroupProtocolMap.get(qualifiedTalkgroup.getProtocol());
|
||||
|
||||
if(p25TalkgroupAliasList == null)
|
||||
{
|
||||
p25TalkgroupAliasList = new TalkgroupAliasList();
|
||||
mTalkgroupProtocolMap.put(qualifiedTalkgroup.getProtocol(), p25TalkgroupAliasList);
|
||||
}
|
||||
|
||||
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
|
||||
break;
|
||||
case RADIO_ID:
|
||||
Radio radio = (Radio)id;
|
||||
|
||||
@@ -444,7 +474,7 @@ public class AliasList
|
||||
return toList(radioAliasList.getAlias(radio));
|
||||
}
|
||||
break;
|
||||
case DCS:
|
||||
case ESN:
|
||||
if(identifier instanceof ESNIdentifier)
|
||||
{
|
||||
return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
|
||||
@@ -623,6 +653,7 @@ public class AliasList
|
||||
*/
|
||||
public class TalkgroupAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -632,6 +663,11 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||
{
|
||||
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||
{
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
|
||||
}
|
||||
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||
@@ -653,26 +689,55 @@ public class AliasList
|
||||
|
||||
public void add(Talkgroup talkgroup, Alias alias)
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
fqt.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedTalkgroup existingFqt &&
|
||||
existingFqt.getHashKey().contentEquals(fqt.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedTalkgroupAliasMap.put(fqt.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
||||
@@ -705,6 +770,7 @@ public class AliasList
|
||||
*/
|
||||
public class RadioAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -714,6 +780,11 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(RadioIdentifier identifier)
|
||||
{
|
||||
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||
{
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
|
||||
}
|
||||
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mRadioAliasMap.get(value);
|
||||
@@ -735,26 +806,55 @@ public class AliasList
|
||||
|
||||
public void add(Radio radio, Alias alias)
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
|
||||
fqr.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
|
||||
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(RadioRange radioRange, Alias alias)
|
||||
|
||||
@@ -35,11 +35,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -61,6 +63,8 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||
|
||||
@@ -29,6 +29,8 @@ public enum AliasIDType
|
||||
LOJACK("LoJack"),
|
||||
LTR_NET_UID("LTR-Net UID"),
|
||||
MIN("Passport MIN"),
|
||||
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||
PRIORITY("Audio Priority"),
|
||||
RADIO_ID("Radio ID"),
|
||||
RADIO_ID_RANGE("Radio ID Range"),
|
||||
@@ -56,7 +58,7 @@ public enum AliasIDType
|
||||
|
||||
//Values used by the View-By alias editor
|
||||
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
||||
UNIT_STATUS, STATUS, TONES);
|
||||
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||
|
||||
public String toString()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.radio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified radio. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedRadio extends Radio
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedRadio()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param id for the radio
|
||||
*/
|
||||
public P25FullyQualifiedRadio(int wacn, int system, int id)
|
||||
{
|
||||
super(Protocol.APCO25, id);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_RADIO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.talkgroup;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified talkgroup. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedTalkgroup extends Talkgroup
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedTalkgroup()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param talkgroup value
|
||||
*/
|
||||
public P25FullyQualifiedTalkgroup(int wacn, int system, int talkgroup)
|
||||
{
|
||||
super(Protocol.APCO25, talkgroup);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_TALKGROUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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.CallManagementPreference;
|
||||
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;
|
||||
@@ -42,6 +39,8 @@ import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
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 +51,18 @@ 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 CallManagementPreference mCallManagementPreference;
|
||||
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
|
||||
|
||||
public DuplicateCallDetector(UserPreferences userPreferences)
|
||||
{
|
||||
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
mCallManagementPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
|
||||
if(mCallManagementPreference.isDuplicateCallDetectionEnabled())
|
||||
{
|
||||
Identifier identifier = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
|
||||
@@ -137,7 +136,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
if(mCallManagementPreference.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
{
|
||||
//Step 1 check for duplicate TO values
|
||||
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
@@ -149,7 +148,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
|
||||
if(mCallManagementPreference.isDuplicateCallDetectionByRadioEnabled())
|
||||
{
|
||||
//Step 2 check for duplicate FROM values
|
||||
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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
|
||||
@@ -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.getDuplicateCallDetectionPreference()
|
||||
.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
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||
|
||||
@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
@@ -67,6 +70,9 @@ public class BroadcastFactory
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
@@ -109,6 +115,8 @@ public class BroadcastFactory
|
||||
return new RdioScannerConfiguration(format);
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyFeedConfiguration(format);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzConfiguration(format);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPConfiguration(format);
|
||||
case ICECAST_TCP:
|
||||
|
||||
@@ -32,6 +32,7 @@ public enum BroadcastServerType
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
/**
|
||||
* HTTP headers used for posting to Rdio Scanner API
|
||||
*/
|
||||
public enum FormField
|
||||
{
|
||||
CALL("call"),
|
||||
FREQ("freq"),
|
||||
START_TIME("start_time"),
|
||||
STOP_TIME("stop_time"),
|
||||
CALL_LENGTH("call_length"),
|
||||
TALKGROUP_NUM("talkgroup_num"),
|
||||
EMERGENCY("emergency"),
|
||||
API_KEY("api_key"),
|
||||
SOURCE_LIST("source_list"),
|
||||
FREQ_LIST("freq_list"),
|
||||
PATCHES("patches");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
FormField(String header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
|
||||
*
|
||||
*/
|
||||
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
|
||||
|
||||
private static final String ENCODING_TYPE_MP3 = "mp3";
|
||||
private static final String MULTIPART_TYPE = "multipart";
|
||||
private static final String DEFAULT_SUBTYPE = "form-data";
|
||||
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
|
||||
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
|
||||
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
|
||||
private HttpClient mHttpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
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 + "]");
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
|
||||
if(mAudioRecordingProcessorFuture == null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
|
||||
0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioRecordingProcessorFuture != null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture.cancel(true);
|
||||
mAudioRecordingProcessorFuture = null;
|
||||
dispose();
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for disposal
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
audioRecording.removePendingReplay();
|
||||
audioRecording = mAudioRecordingQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
|
||||
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
|
||||
* the audio processor thread will persistently invoke this method to attempt a reconnect.
|
||||
*/
|
||||
private boolean connected()
|
||||
{
|
||||
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioQueueSize()
|
||||
{
|
||||
return mAudioRecordingQueue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
mAudioRecordingQueue.offer(audioRecording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
|
||||
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
|
||||
* deleted to ensure that the in-memory queue size doesn't blow up.
|
||||
* @param audioRecording to test
|
||||
* @return true if the recording is valid
|
||||
*/
|
||||
private boolean isValid(AudioRecording audioRecording)
|
||||
{
|
||||
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
|
||||
getBroadcastConfiguration().getMaximumRecordingAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any enqueued audio recordings. This method employs asynchronous
|
||||
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
|
||||
*/
|
||||
private void processRecordingQueue()
|
||||
{
|
||||
|
||||
|
||||
while(connected() && !mAudioRecordingQueue.isEmpty())
|
||||
{
|
||||
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
|
||||
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
|
||||
{
|
||||
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||
String systemLabel = getSystemLabel(audioRecording);
|
||||
|
||||
try
|
||||
{
|
||||
byte[] audioBytes = null;
|
||||
|
||||
try
|
||||
{
|
||||
audioBytes = Files.readAllBytes(audioRecording.getPath());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
|
||||
}
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
|
||||
|
||||
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
|
||||
bodyBuilder
|
||||
.addFile(audioBytes)
|
||||
.addPart(FormField.FREQ, frequency)
|
||||
.addPart(FormField.START_TIME, timestampSeconds)
|
||||
.addPart(FormField.STOP_TIME, timestampSeconds)
|
||||
.addPart(FormField.CALL_LENGTH, durationSeconds)
|
||||
.addPart(FormField.TALKGROUP_NUM, talkgroup)
|
||||
.addPart(FormField.EMERGENCY, 0)
|
||||
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
// For future use if OpenMHz supports patch information
|
||||
//.addPart(FormField.PATCHES, patches)
|
||||
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((fileResponse, throwable1) -> {
|
||||
if(throwable1 != null || fileResponse.statusCode() != 200)
|
||||
{
|
||||
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
|
||||
{
|
||||
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||
//fully read our request and immediately responds.
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if(fileResponse.statusCode() == 200)
|
||||
{
|
||||
incrementStreamedAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Register an error for the file not found exception
|
||||
mLog.error("OpenMHz API - upload file not found [" +
|
||||
audioRecording.getPath().toString() + "]");
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unknown Error", e);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
|
||||
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
if(isValid(audioRecording))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
|
||||
mAudioRecordingQueue.poll();
|
||||
audioRecording.removePendingReplay();
|
||||
incrementAgedOffAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||
audioRecording = mAudioRecordingQueue.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a frequency value from the audio recording identifier collection.
|
||||
*/
|
||||
private static Long getFrequency(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||
Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||
|
||||
if(identifier instanceof ConfigurationLongIdentifier)
|
||||
{
|
||||
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return Long.valueOf(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
|
||||
*/
|
||||
private static String getFrom(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof RadioIdentifier)
|
||||
{
|
||||
return ((RadioIdentifier)identifier).getValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
|
||||
}
|
||||
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
|
||||
{
|
||||
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
|
||||
talkgroupIdentifier.getProtocol()));
|
||||
}
|
||||
else if(identifier instanceof RadioIdentifier radioIdentifier)
|
||||
{
|
||||
return radioIdentifier.getValue().toString();
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupLabel(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupGroup(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0).getGroup());
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the System Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
|
||||
*
|
||||
*/
|
||||
private String getSystemLabel(AudioRecording audioRecording)
|
||||
{
|
||||
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(!systems.isEmpty())
|
||||
{
|
||||
sb.append(systems.get(0));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the patched talkgroups
|
||||
*/
|
||||
public static String getPatches(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
return "[]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests both the connection and configuration against the OpenMHz Call API service
|
||||
* @param configuration containing API key and system id
|
||||
* @return error string or null if test is successful
|
||||
*/
|
||||
public static String testConnection(OpenMHzConfiguration configuration)
|
||||
{
|
||||
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
|
||||
.build();
|
||||
|
||||
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
|
||||
if (response.statusCode() == 200)
|
||||
{
|
||||
return "OK";
|
||||
}
|
||||
else if(response.statusCode() == 403)
|
||||
{
|
||||
return "Invalid API Key";
|
||||
}
|
||||
else if(response.statusCode() == 500)
|
||||
{
|
||||
return "Invalid System Name";
|
||||
}
|
||||
|
||||
return "No Response";
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
return "No Response";
|
||||
}
|
||||
|
||||
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
|
||||
return "Uknown Exception";
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioRecordingProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
processRecordingQueue();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
OpenMHzConfiguration config = new OpenMHzConfiguration();
|
||||
config.setHost("https://api.OpenMHz.com/call-upload-dev");
|
||||
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
|
||||
config.setSystemName("systemx");
|
||||
|
||||
String response = testConnection(config);
|
||||
mLog.error("Response: " + response);
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
mLog.debug("Test Successful!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(response.contains("Invalid API Key"))
|
||||
{
|
||||
mLog.error("Invalid API Key");
|
||||
}
|
||||
else if(response.contains("Invalid System Name"))
|
||||
{
|
||||
mLog.error("Invalid System Name");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Response: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.lang.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Builder for an HTTP body publisher to produce a OpenMHz call event
|
||||
*/
|
||||
public class OpenMHzBuilder
|
||||
{
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
private byte[] audioBytes = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public OpenMHzBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the static multi-part boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio bytes part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addFile(byte[] value)
|
||||
{
|
||||
audioBytes = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, String value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, Number value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value.toString()));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form-data item
|
||||
*/
|
||||
private static String formatPart(Part part, String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
|
||||
sb.append(part.getValue()).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the audio file item
|
||||
*/
|
||||
private String formatFilePart(String boundary)
|
||||
{
|
||||
|
||||
if(audioBytes == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb= new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append("call").append("\"; filename=\"audio.mp3\"\r\n\r\n");
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the boundary closing item
|
||||
*/
|
||||
private static String getClosingBoundary(String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BodyPublisher for accessing the call form data
|
||||
*/
|
||||
public HttpRequest.BodyPublisher build()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(formatFilePart(BOUNDARY));
|
||||
|
||||
|
||||
/**
|
||||
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
|
||||
*/
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
outputStream.write(audioBytes);
|
||||
}
|
||||
|
||||
sb = new StringBuilder();
|
||||
sb.append("\r\n");
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
|
||||
}
|
||||
|
||||
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key:Value pair holder
|
||||
*/
|
||||
public class Part
|
||||
{
|
||||
private String mKey;
|
||||
private String mValue;
|
||||
|
||||
/**
|
||||
* Constructs a new part
|
||||
* @param key value
|
||||
* @param value item
|
||||
*/
|
||||
public Part(String key, String value)
|
||||
{
|
||||
mKey = key;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Streaming configuration for Rdio Scanner API.
|
||||
*
|
||||
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
|
||||
* with the structure of the audio streaming subsystem in sdrtrunk
|
||||
*/
|
||||
public class OpenMHzConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
public static final String PRODUCTION_ENDPOINT = "https://api.openmhz.com";
|
||||
|
||||
private StringProperty mSystemName = new SimpleStringProperty();
|
||||
private StringProperty mApiKey = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Constructor for faster jackson
|
||||
*/
|
||||
public OpenMHzConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
* @param format to use for audio recording (MP3)
|
||||
*/
|
||||
public OpenMHzConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
if(getHost() == null)
|
||||
{
|
||||
setHost(PRODUCTION_ENDPOINT);
|
||||
}
|
||||
|
||||
//The parent class binds this property, so we unbind it and rebind it here
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.and(Bindings.and(Bindings.isNotNull(mSystemName), Bindings.isNotNull(mApiKey)),
|
||||
Bindings.isNotNull(mHost)));
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as a property
|
||||
*/
|
||||
public StringProperty systemIDProperty()
|
||||
{
|
||||
return mSystemName;
|
||||
}
|
||||
|
||||
/**
|
||||
* API key as a property
|
||||
*/
|
||||
public StringProperty apiKeyProperty()
|
||||
{
|
||||
return mApiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
|
||||
public String getApiKey()
|
||||
{
|
||||
return mApiKey.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the api key
|
||||
* @param apiKey
|
||||
*/
|
||||
public void setApiKey(String apiKey)
|
||||
{
|
||||
mApiKey.setValue(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* System name as provided by OpenMHz.com
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system_name")
|
||||
public String getSystemName()
|
||||
{
|
||||
return mSystemName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system name provided by OpenMHz.com
|
||||
*/
|
||||
public void setSystemName(String systemName)
|
||||
{
|
||||
mSystemName.set(systemName);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzConfiguration copy = new OpenMHzConfiguration();
|
||||
copy.setSystemName(getSystemName());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
/**
|
||||
* OpenMHz calls API configuration editor
|
||||
*/
|
||||
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
|
||||
private TextField mSystemNameTextField;
|
||||
private IntegerTextField mMaxAgeTextField;
|
||||
private TextField mApiKeyTextField;
|
||||
private TextField mHostTextField;
|
||||
private GridPane mEditorPane;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for accessing the broadcast model
|
||||
*/
|
||||
public OpenMHzEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(OpenMHzConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getSystemNameTextField().setDisable(item == null);
|
||||
getApiKeyTextField().setDisable(item == null);
|
||||
getHostTextField().setDisable(item == null);
|
||||
getMaxAgeTextField().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getSystemNameTextField().setText(item.getSystemName());
|
||||
getApiKeyTextField().setText(item.getApiKey());
|
||||
getHostTextField().setText(item.getHost());
|
||||
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
|
||||
}
|
||||
else
|
||||
{
|
||||
getSystemNameTextField().setText(null);
|
||||
getApiKeyTextField().setText(null);
|
||||
getHostTextField().setText(null);
|
||||
getMaxAgeTextField().set(0);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystemName(getSystemNameTextField().getText());
|
||||
getItem().setHost(getHostTextField().getText());
|
||||
getItem().setApiKey(getApiKeyTextField().getText());
|
||||
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
|
||||
}
|
||||
|
||||
super.save();
|
||||
}
|
||||
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 5, 10,10));
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(5);
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label formatLabel = new Label("Format");
|
||||
GridPane.setHalignment(formatLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(formatLabel, 0, row);
|
||||
mEditorPane.getChildren().add(formatLabel);
|
||||
|
||||
GridPane.setConstraints(getFormatField(), 1, row);
|
||||
mEditorPane.getChildren().add(getFormatField());
|
||||
|
||||
Label enabledLabel = new Label("Enabled");
|
||||
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(enabledLabel, 2, row);
|
||||
mEditorPane.getChildren().add(enabledLabel);
|
||||
|
||||
GridPane.setConstraints(getEnabledSwitch(), 3, row);
|
||||
mEditorPane.getChildren().add(getEnabledSwitch());
|
||||
|
||||
Label systemLabel = new Label("Name");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getNameTextField());
|
||||
|
||||
Label apiKeyLabel = new Label("API Key");
|
||||
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(apiKeyLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(apiKeyLabel);
|
||||
|
||||
GridPane.setConstraints(getApiKeyTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getApiKeyTextField());
|
||||
|
||||
Label systemNameLabel = new Label("System Short Name");
|
||||
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemNameLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemNameLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getSystemNameTextField());
|
||||
|
||||
Label hostLabel = new Label("OpenMHz Upload Server (API)");
|
||||
GridPane.setHalignment(hostLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(hostLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(hostLabel);
|
||||
|
||||
GridPane.setConstraints(getHostTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getHostTextField());
|
||||
|
||||
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
|
||||
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(maxAgeLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(maxAgeLabel);
|
||||
|
||||
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getMaxAgeTextField());
|
||||
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private IntegerTextField getMaxAgeTextField()
|
||||
{
|
||||
if(mMaxAgeTextField == null)
|
||||
{
|
||||
mMaxAgeTextField = new IntegerTextField();
|
||||
mMaxAgeTextField.setDisable(true);
|
||||
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mMaxAgeTextField;
|
||||
}
|
||||
|
||||
private TextField getHostTextField()
|
||||
{
|
||||
if(mHostTextField == null)
|
||||
{
|
||||
mHostTextField = new TextField();
|
||||
mHostTextField.setDisable(true);
|
||||
mHostTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mHostTextField;
|
||||
}
|
||||
|
||||
private TextField getApiKeyTextField()
|
||||
{
|
||||
if(mApiKeyTextField == null)
|
||||
{
|
||||
mApiKeyTextField = new TextField();
|
||||
mApiKeyTextField.setDisable(true);
|
||||
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mApiKeyTextField;
|
||||
}
|
||||
|
||||
private TextField getSystemNameTextField()
|
||||
{
|
||||
if(mSystemNameTextField == null)
|
||||
{
|
||||
mSystemNameTextField = new TextField();
|
||||
mSystemNameTextField.setDisable(true);
|
||||
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mSystemNameTextField;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenMHzFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
|
||||
public OpenMHzFeedConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenMHz configuration for Icecast 2.3.2 compatible servers
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public OpenMHzFeedConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
setBitRate(16);
|
||||
setChannels(1);
|
||||
setSampleRate(8000);
|
||||
setInline(true);
|
||||
}
|
||||
|
||||
public static OpenMHzFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
OpenMHzFeedConfiguration config = new OpenMHzFeedConfiguration(BroadcastFormat.MP3);
|
||||
config.setName(userFeedBroadcast.getDescription());
|
||||
config.setHost(userFeedBroadcast.getHostname());
|
||||
config.setMountPoint(userFeedBroadcast.getMount());
|
||||
config.setFeedID(userFeedBroadcast.getFeedId());
|
||||
config.setPassword(userFeedBroadcast.getPassword());
|
||||
|
||||
try
|
||||
{
|
||||
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzFeedConfiguration copy = new OpenMHzFeedConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setInline(getInline());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setURL(getURL());
|
||||
|
||||
//OpenMHz Configuration Parameters
|
||||
copy.setFeedID(getFeedID());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
|
||||
public int getFeedID()
|
||||
{
|
||||
return mFeedID;
|
||||
}
|
||||
|
||||
public void setFeedID(int feedID)
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class RdioScannerBuilder
|
||||
}
|
||||
StringBuilder sb= new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; filename =\"").append(audioName);
|
||||
sb.append("Content-Disposition: form-data; filename=\"").append(audioName);
|
||||
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
|
||||
return sb.toString();
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ public class MBECallSequenceConverter
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.BitSet;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
@@ -306,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
|
||||
@@ -1197,9 +1198,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);
|
||||
@@ -1278,7 +1292,7 @@ public class BinaryMessage extends BitSet
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
|
||||
}
|
||||
|
||||
return new String(bytes);
|
||||
return new String(bytes, Charset.forName("UTF-16"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,11 +35,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
|
||||
@@ -66,17 +65,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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
* Swing panel for Now Playing channels table and channel details tab set.
|
||||
*/
|
||||
public class NowPlayingPanel extends JPanel
|
||||
{
|
||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||
@@ -40,45 +43,95 @@ public class NowPlayingPanel extends JPanel
|
||||
private final DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel;
|
||||
private final ChannelPowerPanel mChannelPowerPanel;
|
||||
private JideTabbedPane mTabbedPane;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mDetailTabsVisible;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
* messages, events, and spectral view.
|
||||
*/
|
||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||
SettingsManager settingsManager)
|
||||
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||
{
|
||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||
mDetailTabsVisible = detailTabsVisible;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of the channel details tabs panel.
|
||||
* @param visible true to show or false to hide.
|
||||
*/
|
||||
public void setDetailTabsVisible(boolean visible)
|
||||
{
|
||||
//Only adjust if there is a change in state
|
||||
if(visible ^ mDetailTabsVisible)
|
||||
{
|
||||
mDetailTabsVisible = visible;
|
||||
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
else
|
||||
{
|
||||
getSplitPane().remove(getTabbedPane());
|
||||
}
|
||||
|
||||
revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private JideTabbedPane getTabbedPane()
|
||||
{
|
||||
if(mTabbedPane == null)
|
||||
{
|
||||
mTabbedPane = new JideTabbedPane();
|
||||
mTabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
mTabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
}
|
||||
|
||||
return mTabbedPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split pane for channels table and channel details tabs.
|
||||
*/
|
||||
private JideSplitPane getSplitPane()
|
||||
{
|
||||
if(mSplitPane == null)
|
||||
{
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
mSplitPane.setShowGripper(true);
|
||||
}
|
||||
|
||||
return mSplitPane;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
getSplitPane().add(mChannelMetadataPanel);
|
||||
|
||||
JideTabbedPane tabbedPane = new JideTabbedPane();
|
||||
tabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
tabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
tabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
tabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
tabbedPane.setFont(this.getFont());
|
||||
tabbedPane.setForeground(Color.BLACK);
|
||||
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
|
||||
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
splitPane.setShowGripper(true);
|
||||
splitPane.add(mChannelMetadataPanel);
|
||||
splitPane.add(tabbedPane);
|
||||
add(splitPane);
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
|
||||
add(getSplitPane());
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
|
||||
@@ -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;
|
||||
@@ -47,9 +44,37 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
protected Broadcaster<IDecodeEvent> mDecodeEventBroadcaster = new Broadcaster<>();
|
||||
protected Listener<DecoderStateEvent> mDecoderStateListener;
|
||||
private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener();
|
||||
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
|
||||
*/
|
||||
@@ -145,4 +170,22 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Message listener that only passes messages while we're running. This is important because each of the decoders
|
||||
* can process blocks of samples and that can result in additional messages being generated even after shutdown
|
||||
* and so we shut off the processing of decoded messages when we're commanded to stop. The sample processing thread
|
||||
* cannot be shutdown or forcefully interrupted because downstream decoder and channel states may have acquired
|
||||
* locks that have to be properly released.
|
||||
*/
|
||||
private class MessageListener implements Listener<IMessage>
|
||||
{
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(isRunning())
|
||||
{
|
||||
receive(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -54,6 +54,7 @@ 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();
|
||||
}
|
||||
@@ -95,7 +96,7 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
/**
|
||||
* Sets the current channel descriptor
|
||||
*/
|
||||
protected void setCurrentChannel(IChannelDescriptor channel)
|
||||
public void setCurrentChannel(IChannelDescriptor channel)
|
||||
{
|
||||
mCurrentChannel = channel;
|
||||
}
|
||||
|
||||
@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||
TunerManager tunerManager, UserPreferences userPreferences)
|
||||
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||
{
|
||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||
playlistManager.getAliasModel());
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
|
||||
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Now playing panel.
|
||||
*/
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
{
|
||||
return mNowPlayingPanel;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
||||
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
|
||||
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
|
||||
//Set preferred size to influence the split between these panels
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
@@ -442,7 +445,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 +559,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 +587,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
removed = mProcessingChains.remove(channel);
|
||||
removed = mProcessingChainsMap.remove(channel);
|
||||
|
||||
if(removed != null)
|
||||
{
|
||||
@@ -633,6 +636,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 +665,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 +688,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 +702,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 +808,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 +846,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();
|
||||
|
||||
@@ -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,18 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
@@ -177,6 +180,28 @@ public class CRCDMR
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the residual CRC value using a mask of zero which should identify the mask value when an alternate
|
||||
* mask value is employed.
|
||||
* @param message to be checked
|
||||
* @param messageStart location
|
||||
* @param crcStart location
|
||||
* @return residual CRC check value.
|
||||
*/
|
||||
public static int calculateResidual(CorrectedBinaryMessage message, int messageStart, int crcStart)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
|
||||
}
|
||||
|
||||
int checksum = getIntChecksum(message, crcStart, 16);
|
||||
return calculated ^ checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
|
||||
* data blocks have a slightly complicated structure because the checksum
|
||||
|
||||
@@ -41,6 +41,8 @@ import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.monitor.StatusBox;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
@@ -90,6 +92,7 @@ public class JavaFxWindowManager extends Application
|
||||
private Stage mPlaylistStage;
|
||||
private Stage mUserPreferencesStage;
|
||||
private Stage mRecordingViewerStage;
|
||||
private JFXPanel mStatusPanel;
|
||||
|
||||
/**
|
||||
* Constructs an instance. Note: this constructor is used for Swing applications.
|
||||
@@ -118,6 +121,27 @@ public class JavaFxWindowManager extends Application
|
||||
setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or accesses the JavaFX status panel, used by the main application GUI.
|
||||
* @param resourceMonitor for statistics
|
||||
* @return JFXPanel accessible on Swing thread that delegates JavaFX scene creation to the FX event thread.
|
||||
*/
|
||||
public JFXPanel getStatusPanel(ResourceMonitor resourceMonitor)
|
||||
{
|
||||
if(mStatusPanel == null)
|
||||
{
|
||||
mStatusPanel = new JFXPanel();
|
||||
|
||||
//JFXPanel has to be populated on the FX event thread
|
||||
Platform.runLater(() -> {
|
||||
Scene scene = new Scene(new StatusBox(resourceMonitor));
|
||||
mStatusPanel.setScene(scene);
|
||||
});
|
||||
}
|
||||
|
||||
return mStatusPanel;
|
||||
}
|
||||
|
||||
private void setup()
|
||||
{
|
||||
//Register this class to receive events via each method annotated with @Subscribe
|
||||
@@ -244,9 +268,7 @@ public class JavaFxWindowManager extends Application
|
||||
else
|
||||
{
|
||||
execute(() -> {
|
||||
getJmbeEditorStage().show();
|
||||
getJmbeEditorStage().requestFocus();
|
||||
getJmbeEditorStage().toFront();
|
||||
restoreStage(getJmbeEditorStage());
|
||||
getJmbeEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -317,9 +339,7 @@ public class JavaFxWindowManager extends Application
|
||||
execute(() -> {
|
||||
try
|
||||
{
|
||||
getPlaylistStage().show();
|
||||
getPlaylistStage().requestFocus();
|
||||
getPlaylistStage().toFront();
|
||||
restoreStage(getPlaylistStage());
|
||||
getPlaylistEditor().process(request);
|
||||
}
|
||||
catch(Throwable t)
|
||||
@@ -367,9 +387,7 @@ public class JavaFxWindowManager extends Application
|
||||
public void process(final ViewUserPreferenceEditorRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getUserPreferencesStage().show();
|
||||
getUserPreferencesStage().requestFocus();
|
||||
getUserPreferencesStage().toFront();
|
||||
restoreStage(getUserPreferencesStage());
|
||||
getUserPreferencesEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -408,11 +426,7 @@ public class JavaFxWindowManager extends Application
|
||||
@Subscribe
|
||||
public void process(final ViewIconManagerRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getIconManagerStage().show();
|
||||
getIconManagerStage().requestFocus();
|
||||
getIconManagerStage().toFront();
|
||||
});
|
||||
execute(() -> restoreStage(getIconManagerStage()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,9 +436,7 @@ public class JavaFxWindowManager extends Application
|
||||
public void process(final ViewChannelMapEditorRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getChannelMapStage().show();
|
||||
getChannelMapStage().requestFocus();
|
||||
getChannelMapStage().toFront();
|
||||
restoreStage(getChannelMapStage());
|
||||
getChannelMapEditor().process(request);
|
||||
});
|
||||
}
|
||||
@@ -435,11 +447,22 @@ public class JavaFxWindowManager extends Application
|
||||
@Subscribe
|
||||
public void process(final ViewRecordingViewerRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getRecordingViewerStage().show();
|
||||
getRecordingViewerStage().requestFocus();
|
||||
getRecordingViewerStage().toFront();
|
||||
});
|
||||
execute(() -> restoreStage(getRecordingViewerStage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the stage to previous size and location.
|
||||
* @param stage to restore.
|
||||
*/
|
||||
private void restoreStage(Stage stage)
|
||||
{
|
||||
stage.setIconified(false);
|
||||
stage.show();
|
||||
stage.requestFocus();
|
||||
stage.toFront();
|
||||
stage.setMinHeight(stage.getHeight());
|
||||
stage.setMinWidth(stage.getWidth());
|
||||
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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,8 @@ 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;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
@@ -54,7 +56,7 @@ import io.github.dsheirer.source.tuner.TunerEvent;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||
import io.github.dsheirer.spectrum.ClearTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
@@ -68,11 +70,11 @@ import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Point;
|
||||
import java.awt.Robot;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
@@ -80,7 +82,9 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
@@ -105,18 +109,24 @@ import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
public class SDRTrunk implements Listener<TunerEvent>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunk.class);
|
||||
|
||||
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
|
||||
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
|
||||
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
|
||||
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
|
||||
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
||||
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||
|
||||
private boolean mBroadcastStatusVisible;
|
||||
private boolean mResourceStatusVisible;
|
||||
private boolean mNowPlayingDetailsVisible;
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
private ControllerPanel mControllerPanel;
|
||||
private DiagnosticMonitor mDiagnosticMonitor;
|
||||
private IconModel mIconModel = new IconModel();
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
@@ -127,6 +137,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
private UserPreferences mUserPreferences = new UserPreferences();
|
||||
private TunerManager mTunerManager;
|
||||
private ApplicationLog mApplicationLog;
|
||||
private ResourceMonitor mResourceMonitor;
|
||||
private JFXPanel mResourceStatusPanel;
|
||||
|
||||
private String mTitle;
|
||||
|
||||
@@ -140,6 +152,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||
mApplicationLog.start();
|
||||
|
||||
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||
@@ -155,18 +169,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
}
|
||||
}
|
||||
|
||||
//Setup the application home directory
|
||||
Path home = getHomePath();
|
||||
|
||||
ThreadPool.logSettings();
|
||||
|
||||
mLog.info("Home path: " + home.toString());
|
||||
|
||||
//Load properties file
|
||||
if(home != null)
|
||||
{
|
||||
loadProperties(home);
|
||||
}
|
||||
loadProperties();
|
||||
|
||||
//Log current properties setting
|
||||
SystemProperties.getInstance().logCurrentSettings();
|
||||
@@ -183,7 +189,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);
|
||||
}
|
||||
@@ -213,10 +225,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
MapService mapService = new MapService(mIconModel);
|
||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||
|
||||
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
||||
mSettingsManager, mTunerManager, mUserPreferences);
|
||||
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||
@@ -323,7 +337,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
*/
|
||||
private void initGUI()
|
||||
{
|
||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
|
||||
|
||||
/**
|
||||
* Setup main JFrame window
|
||||
@@ -379,7 +393,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mSplitPane.add(mSpectralPanel);
|
||||
mSplitPane.add(mControllerPanel);
|
||||
|
||||
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
|
||||
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
|
||||
|
||||
//Show broadcast status panel when user requests - disabled by default
|
||||
if(mBroadcastStatusVisible)
|
||||
@@ -389,6 +403,13 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
||||
|
||||
mResourceMonitor.start();
|
||||
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu items
|
||||
*/
|
||||
@@ -398,15 +419,50 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||
exitMenu.addActionListener(
|
||||
new ActionListener()
|
||||
JMenuItem processingStatusReportMenuItem = new JMenuItem("Processing Diagnostic Report");
|
||||
processingStatusReportMenuItem.addActionListener(e -> {
|
||||
try
|
||||
{
|
||||
public void actionPerformed(ActionEvent event)
|
||||
{
|
||||
processShutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
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();
|
||||
System.exit(0);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -415,56 +471,121 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
JMenu viewMenu = new JMenu("View");
|
||||
|
||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||
viewMenu.add(viewPlaylistItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(logsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + logsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewApplicationLogsMenu);
|
||||
|
||||
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
|
||||
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewRecordingsMenuItem.addActionListener(arg0 -> {
|
||||
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
|
||||
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(recordingsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
recordingsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewRecordingsMenuItem);
|
||||
|
||||
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
|
||||
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewEventLogsMenu.addActionListener(arg0 -> {
|
||||
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(eventLogsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + eventLogsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewEventLogsMenu);
|
||||
|
||||
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
|
||||
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
|
||||
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(iconManagerMenu);
|
||||
|
||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
|
||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||
viewMenu.add(recordingViewerMenu);
|
||||
|
||||
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
|
||||
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(settingsMenu);
|
||||
|
||||
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
|
||||
logFilesMenu.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0)
|
||||
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
|
||||
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewScreenCapturesMenu.addActionListener(arg0 -> {
|
||||
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(getHomePath().toFile());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
Desktop.getDesktop().open(screenCapturesDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
getHomePath().toString(),
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + screenCapturesDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(logFilesMenu);
|
||||
viewMenu.add(viewScreenCapturesMenu);
|
||||
|
||||
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||
viewMenu.add(preferencesItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new TunersMenu());
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
|
||||
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
|
||||
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||
@@ -505,6 +626,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,
|
||||
@@ -515,6 +637,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mLog.info("Stopping channels ...");
|
||||
mPlaylistManager.getChannelProcessingManager().shutdown();
|
||||
mAudioRecordingManager.stop();
|
||||
mResourceMonitor.stop();
|
||||
|
||||
mLog.info("Stopping spectral display ...");
|
||||
mSpectralPanel.clearTuner();
|
||||
@@ -560,7 +683,54 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for resource status panel
|
||||
*/
|
||||
private JFXPanel getResourceStatusPanel()
|
||||
{
|
||||
|
||||
if(mResourceStatusPanel == null)
|
||||
{
|
||||
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
|
||||
}
|
||||
|
||||
return mResourceStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the resource status panel at the bottom of the main UI window
|
||||
*/
|
||||
private void toggleResourceStatusPanelVisibility()
|
||||
{
|
||||
mResourceStatusVisible = !mResourceStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.remove(getResourceStatusPanel());
|
||||
}
|
||||
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the Now Playing channel details panel
|
||||
*/
|
||||
private void toggleNowPlayingDetailsPanelVisibility()
|
||||
{
|
||||
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
|
||||
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
|
||||
@@ -568,34 +738,30 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
* Loads the application properties file from the user's home directory,
|
||||
* creating the properties file for the first-time, if necessary
|
||||
*/
|
||||
private void loadProperties(Path homePath)
|
||||
private void loadProperties()
|
||||
{
|
||||
Path propsPath = homePath.resolve("SDRTrunk.properties");
|
||||
Path propertiesPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot().resolve("SDRTrunk.properties");
|
||||
|
||||
if(!Files.exists(propsPath))
|
||||
if(!Files.exists(propertiesPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
mLog.info("SDRTrunk - creating application properties file [" +
|
||||
propsPath.toAbsolutePath() + "]");
|
||||
|
||||
Files.createFile(propsPath);
|
||||
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
|
||||
Files.createFile(propertiesPath);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't create application properties "
|
||||
+ "file [" + propsPath.toAbsolutePath(), e);
|
||||
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if(Files.exists(propsPath))
|
||||
if(Files.exists(propertiesPath))
|
||||
{
|
||||
SystemProperties.getInstance().load(propsPath);
|
||||
SystemProperties.getInstance().load(propertiesPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
|
||||
"application properties file");
|
||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,26 +844,50 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast status panel visible toggle menu item
|
||||
*/
|
||||
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
private ControllerPanel mControllerPanel;
|
||||
|
||||
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
|
||||
public BroadcastStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Streaming Status");
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mControllerPanel = controllerPanel;
|
||||
/**
|
||||
* Resource status panel visible toggle menu item
|
||||
*/
|
||||
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public ResourceStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Resource Status");
|
||||
setSelected(mResourceStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleResourceStatusPanelVisibility();
|
||||
setSelected(mResourceStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(mBroadcastStatusPanel != null);
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
}
|
||||
/**
|
||||
* Now Playing channel details visible toggle menu item
|
||||
*/
|
||||
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public NowPlayingChannelDetailsVisibleMenuItem()
|
||||
{
|
||||
super("Show Now Playing Channel Details");
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
addActionListener(e -> {
|
||||
toggleNowPlayingDetailsPanelVisibility();
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"));
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,13 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
@@ -684,8 +686,10 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
||||
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddP25FullyQualifiedTalkgroupItem());
|
||||
p25Menu.getItems().add(new AddRadioIdItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
||||
p25Menu.getItems().add(new AddP25FullyQualifiedRadioIdItem());
|
||||
p25Menu.getItems().add(new SeparatorMenuItem());
|
||||
p25Menu.getItems().add(new AddUserStatusItem());
|
||||
p25Menu.getItems().add(new AddUnitStatusItem());
|
||||
@@ -1322,6 +1326,44 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Fully Qualified Radio ID alias identifier
|
||||
*/
|
||||
public class AddP25FullyQualifiedRadioIdItem extends MenuItem
|
||||
{
|
||||
public AddP25FullyQualifiedRadioIdItem()
|
||||
{
|
||||
super("Fully Qualified Radio ID");
|
||||
setOnAction(event -> {
|
||||
P25FullyQualifiedRadio radioId = new P25FullyQualifiedRadio();
|
||||
radioId.setProtocol(Protocol.APCO25);
|
||||
getIdentifiersList().getItems().add(radioId);
|
||||
getIdentifiersList().getSelectionModel().select(radioId);
|
||||
getIdentifiersList().scrollTo(radioId);
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Fully Qualified Talkgroup alias identifier
|
||||
*/
|
||||
public class AddP25FullyQualifiedTalkgroupItem extends MenuItem
|
||||
{
|
||||
public AddP25FullyQualifiedTalkgroupItem()
|
||||
{
|
||||
super("Fully Qualified Talkgroup");
|
||||
setOnAction(event -> {
|
||||
P25FullyQualifiedTalkgroup talkgroup = new P25FullyQualifiedTalkgroup();
|
||||
talkgroup.setProtocol(Protocol.APCO25);
|
||||
getIdentifiersList().getItems().add(talkgroup);
|
||||
getIdentifiersList().getSelectionModel().select(talkgroup);
|
||||
getIdentifiersList().scrollTo(talkgroup);
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
|
||||
*/
|
||||
@@ -1505,7 +1547,25 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
if(item instanceof Talkgroup)
|
||||
if(item instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("APCO-25 Fully Qualified Talkgroup:").append(fqt.getWacn());
|
||||
sb.append(".").append(fqt.getSystem());
|
||||
sb.append(".").append(fqt.getValue());
|
||||
|
||||
if(fqt.overlapProperty().get())
|
||||
{
|
||||
sb.append(" - Error: Overlap");
|
||||
}
|
||||
|
||||
if(!fqt.isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof Talkgroup)
|
||||
{
|
||||
Talkgroup talkgroup = (Talkgroup)item;
|
||||
Protocol protocol = talkgroup.getProtocol();
|
||||
@@ -1552,6 +1612,24 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("APCO-25 Fully Qualified Radio ID:").append(fqr.getWacn());
|
||||
sb.append(".").append(fqr.getSystem());
|
||||
sb.append(".").append(fqr.getValue());
|
||||
|
||||
if(fqr.overlapProperty().get())
|
||||
{
|
||||
sb.append(" - Error: Overlap");
|
||||
}
|
||||
|
||||
if(!fqr.isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
setText(sb.toString());
|
||||
}
|
||||
else if(item instanceof Radio)
|
||||
{
|
||||
Radio radio = (Radio)item;
|
||||
|
||||
@@ -41,6 +41,10 @@ public class IdentifierEditorFactory
|
||||
return new EsnEditor();
|
||||
case LOJACK:
|
||||
return new LojackEditor();
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
return new P25FullyQualifiedRadioIdEditor(userPreferences);
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
return new P25FullyQualifiedTalkgroupEditor(userPreferences);
|
||||
case RADIO_ID:
|
||||
return new RadioIdEditor(userPreferences);
|
||||
case RADIO_ID_RANGE:
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.alias.identifier;
|
||||
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.gui.control.HexFormatter;
|
||||
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Editor for P25 fully qualified radio alias identifiers
|
||||
*/
|
||||
public class P25FullyQualifiedRadioIdEditor extends IdentifierEditor<P25FullyQualifiedRadio>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedRadioIdEditor.class);
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private Label mProtocolLabel;
|
||||
private TextField mWacnField;
|
||||
private TextField mSystemField;
|
||||
private TextField mRadioField;
|
||||
private TextFormatter<Integer> mWacnTextFormatter;
|
||||
private TextFormatter<Integer> mSystemTextFormatter;
|
||||
private TextFormatter<Integer> mRadioTextFormatter;
|
||||
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||
private RadioValueChangeListener mRadioValueChangeListener = new RadioValueChangeListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param userPreferences for determining user preferred talkgroup formats
|
||||
*/
|
||||
public P25FullyQualifiedRadioIdEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(3);
|
||||
|
||||
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||
gridPane.getChildren().add(getProtocolLabel());
|
||||
|
||||
Label valueLabel = new Label("WACN");
|
||||
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(valueLabel, 1, 0);
|
||||
gridPane.getChildren().add(valueLabel);
|
||||
|
||||
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||
gridPane.getChildren().add(getWacnField());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 3, 0);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||
gridPane.getChildren().add(getSystemField());
|
||||
|
||||
Label radioLabel = new Label("Radio ID");
|
||||
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(radioLabel, 5, 0);
|
||||
gridPane.getChildren().add(radioLabel);
|
||||
|
||||
GridPane.setConstraints(getRadioField(), 6, 0);
|
||||
gridPane.getChildren().add(getRadioField());
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(P25FullyQualifiedRadio item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
P25FullyQualifiedRadio fqr = getItem();
|
||||
|
||||
getProtocolLabel().setDisable(fqr == null);
|
||||
getWacnField().setDisable(fqr == null);
|
||||
getSystemField().setDisable(fqr == null);
|
||||
getRadioField().setDisable(fqr == null);
|
||||
|
||||
if(fqr != null)
|
||||
{
|
||||
getProtocolLabel().setText(fqr.getProtocol().toString());
|
||||
updateTextFormatter();
|
||||
}
|
||||
else
|
||||
{
|
||||
getWacnField().setText(null);
|
||||
getSystemField().setText(null);
|
||||
getRadioField().setText(null);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
private void updateTextFormatter()
|
||||
{
|
||||
if(mWacnTextFormatter != null)
|
||||
{
|
||||
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||
}
|
||||
if(mSystemTextFormatter != null)
|
||||
{
|
||||
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||
}
|
||||
if(mRadioTextFormatter != null)
|
||||
{
|
||||
mRadioTextFormatter.valueProperty().removeListener(mRadioValueChangeListener);
|
||||
}
|
||||
|
||||
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||
|
||||
if(format == IntegerFormat.DECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof IntegerFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||
mRadioTextFormatter = new IntegerFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||
mRadioField.setTooltip(new Tooltip("Format: 0 - 16777215"));
|
||||
}
|
||||
else if(format == IntegerFormat.HEXADECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof HexFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||
mRadioTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||
mRadioField.setTooltip(new Tooltip("Format: 0 - FFFFFF"));
|
||||
}
|
||||
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||
|
||||
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||
mRadioTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||
|
||||
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||
mRadioTextFormatter.valueProperty().addListener(mRadioValueChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
private Label getProtocolLabel()
|
||||
{
|
||||
if(mProtocolLabel == null)
|
||||
{
|
||||
mProtocolLabel = new Label();
|
||||
}
|
||||
|
||||
return mProtocolLabel;
|
||||
}
|
||||
|
||||
private TextField getWacnField()
|
||||
{
|
||||
if(mWacnField == null)
|
||||
{
|
||||
mWacnField = new TextField();
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
}
|
||||
|
||||
return mWacnField;
|
||||
}
|
||||
|
||||
private TextField getSystemField()
|
||||
{
|
||||
if(mSystemField == null)
|
||||
{
|
||||
mSystemField = new TextField();
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
}
|
||||
|
||||
return mSystemField;
|
||||
}
|
||||
|
||||
private TextField getRadioField()
|
||||
{
|
||||
if(mRadioField == null)
|
||||
{
|
||||
mRadioField = new TextField();
|
||||
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||
}
|
||||
|
||||
return mRadioField;
|
||||
}
|
||||
|
||||
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setWacn(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystem(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RadioValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setValue(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.alias.identifier;
|
||||
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.gui.control.HexFormatter;
|
||||
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Editor for P25 fully qualified talkgroup alias identifiers
|
||||
*/
|
||||
public class P25FullyQualifiedTalkgroupEditor extends IdentifierEditor<P25FullyQualifiedTalkgroup>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedTalkgroupEditor.class);
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private Label mProtocolLabel;
|
||||
private TextField mWacnField;
|
||||
private TextField mSystemField;
|
||||
private TextField mTalkgroupField;
|
||||
private TextFormatter<Integer> mWacnTextFormatter;
|
||||
private TextFormatter<Integer> mSystemTextFormatter;
|
||||
private TextFormatter<Integer> mTalkgroupTextFormatter;
|
||||
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||
private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param userPreferences for determining user preferred talkgroup formats
|
||||
*/
|
||||
public P25FullyQualifiedTalkgroupEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(3);
|
||||
|
||||
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||
gridPane.getChildren().add(getProtocolLabel());
|
||||
|
||||
Label valueLabel = new Label("WACN");
|
||||
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(valueLabel, 1, 0);
|
||||
gridPane.getChildren().add(valueLabel);
|
||||
|
||||
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||
gridPane.getChildren().add(getWacnField());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 3, 0);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||
gridPane.getChildren().add(getSystemField());
|
||||
|
||||
Label radioLabel = new Label("Talkgroup");
|
||||
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(radioLabel, 5, 0);
|
||||
gridPane.getChildren().add(radioLabel);
|
||||
|
||||
GridPane.setConstraints(getTalkgroupField(), 6, 0);
|
||||
gridPane.getChildren().add(getTalkgroupField());
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(P25FullyQualifiedTalkgroup item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
P25FullyQualifiedTalkgroup fqt = getItem();
|
||||
|
||||
getProtocolLabel().setDisable(fqt == null);
|
||||
getWacnField().setDisable(fqt == null);
|
||||
getSystemField().setDisable(fqt == null);
|
||||
getTalkgroupField().setDisable(fqt == null);
|
||||
|
||||
if(fqt != null)
|
||||
{
|
||||
getProtocolLabel().setText(fqt.getProtocol().toString());
|
||||
updateTextFormatter();
|
||||
}
|
||||
else
|
||||
{
|
||||
getWacnField().setText(null);
|
||||
getSystemField().setText(null);
|
||||
getTalkgroupField().setText(null);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
private void updateTextFormatter()
|
||||
{
|
||||
if(mWacnTextFormatter != null)
|
||||
{
|
||||
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||
}
|
||||
if(mSystemTextFormatter != null)
|
||||
{
|
||||
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||
}
|
||||
if(mTalkgroupTextFormatter != null)
|
||||
{
|
||||
mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
|
||||
}
|
||||
|
||||
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||
|
||||
if(format == IntegerFormat.DECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof IntegerFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||
mTalkgroupTextFormatter = new IntegerFormatter(0,0xFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - 65535"));
|
||||
}
|
||||
else if(format == IntegerFormat.HEXADECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof HexFormatter)))
|
||||
{
|
||||
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||
mTalkgroupTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||
|
||||
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - FFFF"));
|
||||
}
|
||||
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||
|
||||
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||
mTalkgroupTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||
|
||||
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||
mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
private Label getProtocolLabel()
|
||||
{
|
||||
if(mProtocolLabel == null)
|
||||
{
|
||||
mProtocolLabel = new Label();
|
||||
}
|
||||
|
||||
return mProtocolLabel;
|
||||
}
|
||||
|
||||
private TextField getWacnField()
|
||||
{
|
||||
if(mWacnField == null)
|
||||
{
|
||||
mWacnField = new TextField();
|
||||
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||
}
|
||||
|
||||
return mWacnField;
|
||||
}
|
||||
|
||||
private TextField getSystemField()
|
||||
{
|
||||
if(mSystemField == null)
|
||||
{
|
||||
mSystemField = new TextField();
|
||||
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||
}
|
||||
|
||||
return mSystemField;
|
||||
}
|
||||
|
||||
private TextField getTalkgroupField()
|
||||
{
|
||||
if(mTalkgroupField == null)
|
||||
{
|
||||
mTalkgroupField = new TextField();
|
||||
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||
}
|
||||
|
||||
return mTalkgroupField;
|
||||
}
|
||||
|
||||
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setWacn(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystem(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TalkgroupValueChangeListener implements ChangeListener<Integer>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setValue(newValue != null ? newValue : 0);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.AgencyInfo;
|
||||
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||
import io.github.dsheirer.service.radioreference.RadioReference;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.application.Platform;
|
||||
@@ -158,8 +159,13 @@ public class AgencyEditor extends VBox
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
try
|
||||
{
|
||||
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||
if (mLevel == Level.COUNTY && agency instanceof CountyAgency) {
|
||||
final CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(-agency.getAgencyId());
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(countyInfo.getCategories()));
|
||||
} else {
|
||||
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
@@ -196,11 +202,11 @@ public class AgencyEditor extends VBox
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if(o1.getName() == null)
|
||||
else if(o1.getName() == null || o2 instanceof CountyAgency)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if(o2.getName() == null)
|
||||
else if(o2.getName() == null || o1 instanceof CountyAgency)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -158,9 +158,12 @@ public class AgencyFrequencyEditor extends GridPane
|
||||
|
||||
if(categories != null && !categories.isEmpty())
|
||||
{
|
||||
getCategoryComboBox().getItems().add(ALL_CATEGORIES);
|
||||
// Only allow the combined list of all categories if there aren't that many
|
||||
if (categories.size() < 10) {
|
||||
categories.add(0, ALL_CATEGORIES);
|
||||
}
|
||||
getCategoryComboBox().getItems().addAll(categories);
|
||||
getCategoryComboBox().getSelectionModel().select(ALL_CATEGORIES);
|
||||
getCategoryComboBox().getSelectionModel().select(categories.get(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.radioreference;
|
||||
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||
|
||||
public class CountyAgency extends Agency
|
||||
{
|
||||
public CountyAgency(CountyInfo countyInfo)
|
||||
{
|
||||
this.setAgencyId(-countyInfo.getCountyId());
|
||||
this.setName(countyInfo.getName() + " County (All)");
|
||||
this.setType(-1);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.radioreference;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.RadioReferenceException;
|
||||
import io.github.dsheirer.rrapi.type.Agency;
|
||||
import io.github.dsheirer.rrapi.type.AuthorizationInformation;
|
||||
import io.github.dsheirer.rrapi.type.Country;
|
||||
import io.github.dsheirer.rrapi.type.CountryInfo;
|
||||
@@ -55,6 +56,7 @@ import jiconfont.javafx.IconNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -689,7 +691,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer<Authori
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getCountySystemEditor().setSystems(countyInfo.getSystems());
|
||||
getCountyAgencyEditor().setAgencies(countyInfo.getAgencies());
|
||||
|
||||
// Make a new list so we don't add our CountyAgency to the original list of agencies
|
||||
List<Agency> countyAgencies = new ArrayList<Agency>();
|
||||
countyAgencies.add(new CountyAgency(countyInfo));
|
||||
countyAgencies.addAll(countyInfo.getAgencies());
|
||||
getCountyAgencyEditor().setAgencies(countyAgencies);
|
||||
});
|
||||
}
|
||||
catch(RadioReferenceException rre)
|
||||
|
||||
@@ -294,7 +294,11 @@ public class SystemEditor extends VBox
|
||||
{
|
||||
CountyInfo countyInfo = null;
|
||||
|
||||
if(site.getCountyId() > 0)
|
||||
int countyId = site.getCountyId();
|
||||
|
||||
//Temporary sites that have been added to a system where the county/location is unknown, can
|
||||
//have a county ID of 99,999 as observed from API (undocumented).
|
||||
if(countyId > 0 && countyId != 99999)
|
||||
{
|
||||
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class StreamEditorFactory
|
||||
return new BroadcastifyStreamEditor(playlistManager);
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerEditor(playlistManager);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzEditor(playlistManager);
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallEditor(playlistManager);
|
||||
case ICECAST_HTTP:
|
||||
|
||||
@@ -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-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
|
||||
@@ -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,23 +37,24 @@ 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
||||
import java.io.File;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -31,6 +32,7 @@ import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -39,8 +41,6 @@ import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
/**
|
||||
* Preference settings for channel event view
|
||||
@@ -97,6 +97,9 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
private Button mResetStreamingButton;
|
||||
private Label mStreamingPathLabel;
|
||||
|
||||
private Spinner<Integer> mRecordingSpinner;
|
||||
private Spinner<Integer> mEventLogSpinner;
|
||||
|
||||
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
||||
@@ -247,11 +250,57 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
|
||||
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
||||
mEditorPane.add(getResetStreamingButton(), 3, row++);
|
||||
|
||||
Label monitorLabel = new Label("File storage usage monitoring - maximum size thresholds (MB)");
|
||||
GridPane.setMargin(monitorLabel, new Insets(15, 0, 2, 0));
|
||||
mEditorPane.add(monitorLabel, 0, row++, 4, 1);
|
||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, row++, 4, 1);
|
||||
|
||||
mEditorPane.add(new Label("Event Logs"), 0, row);
|
||||
GridPane.setMargin(getEventLogSpinner(), new Insets(2, 2, 2, 0));
|
||||
mEditorPane.add(getEventLogSpinner(), 1, row++);
|
||||
|
||||
mEditorPane.add(new Label("Recordings"), 0, row);
|
||||
GridPane.setMargin(getRecordingSpinner(), new Insets(2, 2, 2, 0));
|
||||
mEditorPane.add(getRecordingSpinner(), 1, row);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording directory maximum size threshold spinner
|
||||
* @return spinner
|
||||
*/
|
||||
private Spinner<Integer> getRecordingSpinner()
|
||||
{
|
||||
if(mRecordingSpinner == null)
|
||||
{
|
||||
mRecordingSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageRecordings(), 100);
|
||||
mRecordingSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||
.setDirectoryMaxUsageRecordings(newValue));
|
||||
}
|
||||
|
||||
return mRecordingSpinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event log directory maximum size threshold spinner
|
||||
* @return spinner
|
||||
*/
|
||||
private Spinner<Integer> getEventLogSpinner()
|
||||
{
|
||||
if(mEventLogSpinner == null)
|
||||
{
|
||||
mEventLogSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageEventLogs(), 100);
|
||||
mEventLogSpinner.setEditable(true);
|
||||
mEventLogSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||
.setDirectoryMaxUsageEventLogs(newValue));
|
||||
}
|
||||
|
||||
return mEventLogSpinner;
|
||||
}
|
||||
|
||||
private Label getApplicationRootLabel()
|
||||
{
|
||||
if(mApplicationRootLabel == null)
|
||||
|
||||
@@ -69,9 +69,17 @@ public class RecordingViewer extends VBox
|
||||
|
||||
Menu createNewViewerMenu = new Menu("New Viewer ...");
|
||||
MenuItem dmrMenuItem = new MenuItem("DMR");
|
||||
dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())));
|
||||
dmrMenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
||||
p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())));
|
||||
p25p1MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
|
||||
|
||||
MenuItem exitMenu = new MenuItem("Exit");
|
||||
|
||||
@@ -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,7 +37,6 @@ public enum Form
|
||||
DTMF,
|
||||
ENCRYPTION_KEY,
|
||||
ESN,
|
||||
FULLY_QUALIFIED_IDENTIFIER,
|
||||
IPV4_ADDRESS,
|
||||
KNOX_TONE,
|
||||
LOCATION,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.alias;
|
||||
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Talker alias value provided by the network for the current talker (ie FROM).
|
||||
*/
|
||||
public class P25TalkerAliasIdentifier extends TalkerAliasIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param value of the talker alias
|
||||
*/
|
||||
public P25TalkerAliasIdentifier(String value)
|
||||
{
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return getValue() != null && !getValue().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
public static P25TalkerAliasIdentifier create(String value)
|
||||
{
|
||||
return new P25TalkerAliasIdentifier(value);
|
||||
}
|
||||
}
|
||||
@@ -1,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.log;
|
||||
@@ -30,16 +29,15 @@ import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Logback and SLF4j logging implementation.
|
||||
@@ -182,7 +180,11 @@ public class ApplicationLog
|
||||
}
|
||||
}
|
||||
|
||||
private Attributes findManifestAttributes() {
|
||||
/**
|
||||
* Finds the jar manifest attributes
|
||||
* @return attributes or null.
|
||||
*/
|
||||
public Attributes findManifestAttributes() {
|
||||
try {
|
||||
Enumeration<URL> resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
|
||||
while (resources.hasMoreElements()) {
|
||||
@@ -197,9 +199,12 @@ public class ApplicationLog
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return 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
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.action.AliasActionManager;
|
||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||
import io.github.dsheirer.audio.AudioModule;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
@@ -47,7 +48,11 @@ import io.github.dsheirer.module.decode.dmr.DMRDecoderState;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.message.filter.DmrMessageFilterSet;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter;
|
||||
@@ -120,9 +125,11 @@ public class DecoderFactory
|
||||
* @return list of configured decoders
|
||||
*/
|
||||
public static List<Module> getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
|
||||
UserPreferences userPreferences, TrafficChannelManager trafficChannelManager)
|
||||
UserPreferences userPreferences, TrafficChannelManager trafficChannelManager,
|
||||
IChannelDescriptor channelDescriptor)
|
||||
{
|
||||
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences, trafficChannelManager);
|
||||
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences,
|
||||
trafficChannelManager, channelDescriptor);
|
||||
modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration()));
|
||||
return modules;
|
||||
}
|
||||
@@ -135,10 +142,12 @@ public class DecoderFactory
|
||||
* @param aliasModel for alias lookups
|
||||
* @param userPreferences instance
|
||||
* @param trafficChannelManager optional traffic channel manager to use
|
||||
* @param channelDescriptor to preload into the decoder state as the current channel.
|
||||
* @return list of modules to use for a processing chain
|
||||
*/
|
||||
public static List<Module> getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
|
||||
UserPreferences userPreferences, TrafficChannelManager trafficChannelManager)
|
||||
UserPreferences userPreferences, TrafficChannelManager trafficChannelManager,
|
||||
IChannelDescriptor channelDescriptor)
|
||||
{
|
||||
List<Module> modules = new ArrayList<Module>();
|
||||
|
||||
@@ -157,7 +166,7 @@ public class DecoderFactory
|
||||
break;
|
||||
case DMR:
|
||||
processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
|
||||
trafficChannelManager);
|
||||
trafficChannelManager, channelDescriptor);
|
||||
break;
|
||||
case NBFM:
|
||||
processNBFM(channel, modules, aliasList, decodeConfig);
|
||||
@@ -410,28 +419,82 @@ public class DecoderFactory
|
||||
*/
|
||||
private static void processDMR(Channel channel, UserPreferences userPreferences, List<Module> modules,
|
||||
AliasList aliasList, DecodeConfigDMR decodeConfig,
|
||||
TrafficChannelManager trafficChannelManager)
|
||||
TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor)
|
||||
{
|
||||
modules.add(new DMRDecoder(decodeConfig));
|
||||
|
||||
DMRTrafficChannelManager dmrTrafficChannelManager = null;
|
||||
|
||||
if(trafficChannelManager instanceof DMRTrafficChannelManager)
|
||||
{
|
||||
dmrTrafficChannelManager = (DMRTrafficChannelManager)trafficChannelManager;
|
||||
}
|
||||
else
|
||||
{
|
||||
dmrTrafficChannelManager = new DMRTrafficChannelManager(channel);
|
||||
}
|
||||
|
||||
//Only register the traffic channel manager as a module if this is the parent control channel.
|
||||
if(channel.isStandardChannel())
|
||||
{
|
||||
if(trafficChannelManager instanceof DMRTrafficChannelManager)
|
||||
{
|
||||
dmrTrafficChannelManager = (DMRTrafficChannelManager)trafficChannelManager;
|
||||
}
|
||||
else
|
||||
{
|
||||
dmrTrafficChannelManager = new DMRTrafficChannelManager(channel);
|
||||
}
|
||||
|
||||
modules.add(dmrTrafficChannelManager);
|
||||
}
|
||||
|
||||
modules.add(new DMRDecoderState(channel, 1, dmrTrafficChannelManager));
|
||||
modules.add(new DMRDecoderState(channel, 2, dmrTrafficChannelManager));
|
||||
DMRDecoderState state1 = new DMRDecoderState(channel, 1, dmrTrafficChannelManager);
|
||||
DMRDecoderState state2 = new DMRDecoderState(channel, 2, dmrTrafficChannelManager);
|
||||
|
||||
//Register the states with each other so that they can pass Cap+ site status messaging to resolve current channel
|
||||
state1.setSisterDecoderState(state2);
|
||||
state2.setSisterDecoderState(state1);
|
||||
|
||||
//If an LSN is provided, apply it to both of the decoder states.
|
||||
if(channelDescriptor instanceof DMRLsn lsn)
|
||||
{
|
||||
//If this is a REST descriptor, change it to a standard LSN descriptor.
|
||||
if(channelDescriptor instanceof DmrRestLsn rest)
|
||||
{
|
||||
lsn = new DMRLsn(rest.getLsn());
|
||||
lsn.setTimeslotFrequency(rest.getTimeslotFrequency());
|
||||
}
|
||||
|
||||
if(lsn.getTimeslot() == 1)
|
||||
{
|
||||
state1.setCurrentChannel(lsn);
|
||||
state2.setCurrentChannel(lsn.getSisterTimeslot());
|
||||
}
|
||||
else
|
||||
{
|
||||
state1.setCurrentChannel(lsn.getSisterTimeslot());
|
||||
state2.setCurrentChannel(lsn);
|
||||
}
|
||||
}
|
||||
|
||||
if(decodeConfig.hasChannelGrantEvent())
|
||||
{
|
||||
DecodeEvent event = decodeConfig.getChannelGrantEvent();
|
||||
|
||||
if(decodeConfig.getChannelGrantEvent().getTimeslot() == 1)
|
||||
{
|
||||
state1.setCurrentCallEvent(event);
|
||||
|
||||
if(event.getChannelDescriptor() instanceof DMRChannel dmrChannel)
|
||||
{
|
||||
state2.setCurrentChannel(dmrChannel.getSisterTimeslot());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state2.setCurrentCallEvent(event);
|
||||
|
||||
if(event.getChannelDescriptor() instanceof DMRChannel dmrChannel)
|
||||
{
|
||||
state1.setCurrentChannel(dmrChannel.getSisterTimeslot());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules.add(state1);
|
||||
modules.add(state2);
|
||||
modules.add(new DMRAudioModule(userPreferences, aliasList, 1));
|
||||
modules.add(new DMRAudioModule(userPreferences, aliasList, 2));
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -145,11 +145,10 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
super.start();
|
||||
getIdentifierCollection().update(getChannelNameIdentifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {}
|
||||
@Override
|
||||
public void init() {}
|
||||
@Override
|
||||
|
||||
@@ -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
|
||||
@@ -115,9 +115,4 @@ public class DCSDecoderState extends DecoderState
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -284,11 +284,27 @@ public class DMRBurstFramer implements Listener<Dibit>
|
||||
}
|
||||
else
|
||||
{
|
||||
//When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
|
||||
//don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
|
||||
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
|
||||
//repeater or we have some form of mobile burst. If mobile, process to the correct timeslto, otherwise
|
||||
//dump the messages to timeslot 0 so that the burst doesn't corrupt the decoder states for either
|
||||
// timeslot, until we positively regain timeslot tracking
|
||||
if(burst1 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0);
|
||||
DMRSyncPattern sync1 = mSyncTrackerTimeslot1.getSyncPattern();
|
||||
|
||||
if(sync1.isMobileStationSyncPattern() || sync1.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 1);
|
||||
}
|
||||
else if(sync1.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
@@ -298,7 +314,21 @@ public class DMRBurstFramer implements Listener<Dibit>
|
||||
|
||||
if(burst2 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0);
|
||||
DMRSyncPattern sync2 = mSyncTrackerTimeslot2.getSyncPattern();
|
||||
|
||||
if(sync2.isMobileStationSyncPattern() || sync2.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 1);
|
||||
}
|
||||
else if(sync2.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -42,8 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -332,4 +335,38 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
{
|
||||
return DMRDecoder.this;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
File file = new File("/home/denny/Downloads/TIII,HYT414.585000,.wav");
|
||||
|
||||
try
|
||||
{
|
||||
ComplexWaveSource source = new ComplexWaveSource(file);
|
||||
source.open();
|
||||
System.out.println("Source Sample Rate: " + source.getSampleRate());
|
||||
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR());
|
||||
decoder.setMessageListener(message -> System.out.println("TS:" + message.getTimeslot() + " " + message));
|
||||
source.setListener(iNativeBuffer -> {
|
||||
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
ComplexSamples samples = it.next();
|
||||
decoder.receive(samples);
|
||||
}
|
||||
});
|
||||
decoder.setSampleRate(source.getSampleRate());
|
||||
decoder.start();
|
||||
source.start();
|
||||
|
||||
while(true)
|
||||
{
|
||||
source.next(65535);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -33,16 +33,24 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.event.DMRDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.UnknownCSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAdvantageModeVoiceChannelUpdate;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxOpenModeVoiceChannelUpdate;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant;
|
||||
@@ -65,6 +73,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasComp
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
|
||||
@@ -79,6 +90,7 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParam
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
|
||||
@@ -90,7 +102,6 @@ import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
|
||||
import io.github.dsheirer.util.PacketUtil;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||
@@ -104,16 +115,17 @@ import org.slf4j.LoggerFactory;
|
||||
public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRDecoderState.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||
private static final long MAX_VALID_CALL_DURATION_MS = 30000;
|
||||
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
|
||||
new AddChannelRotationActiveStateRequest(State.ACTIVE);
|
||||
private Channel mChannel;
|
||||
private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor;
|
||||
private DMRTrafficChannelManager mTrafficChannelManager;
|
||||
private DecodeEvent mCurrentCallEvent;
|
||||
private long mCurrentFrequency;
|
||||
private boolean mIgnoreCRCChecksums;
|
||||
private Map<DMRChannel,DecodeEvent> mDetectedCallEventsMap = new HashMap<>();
|
||||
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
|
||||
new AddChannelRotationActiveStateRequest(State.ACTIVE);
|
||||
private DMRDecoderState mSisterDecoderState;
|
||||
|
||||
/**
|
||||
* Constructs an DMR decoder state with an optional traffic channel manager.
|
||||
@@ -141,6 +153,75 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a reference to the decoder state that is processing the sisten timeslot. If this state is covering
|
||||
* timeslot 1, then the argument will be the state covering timeslot 2, and vice-versa.
|
||||
*/
|
||||
public void setSisterDecoderState(DMRDecoderState state)
|
||||
{
|
||||
mSisterDecoderState = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a map of active talkgroups received from the sister timeslot decoder state so that we can recover
|
||||
* the current channel identifier to enable populating call events with accurate current channel info.
|
||||
*
|
||||
* This is used for Capacity Plus systems to recover the current channel.
|
||||
*
|
||||
* @param idMap to process
|
||||
* @param lsnMap to process
|
||||
*/
|
||||
public DMRChannel processActiveTalkgroups(Map<Integer, IntegerIdentifier> idMap, Map<Integer, DMRLsn> lsnMap)
|
||||
{
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
List<Identifier> toIds = mCurrentCallEvent.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
|
||||
if(toIds.size() >= 1)
|
||||
{
|
||||
Identifier to = toIds.get(0);
|
||||
|
||||
if(to instanceof TalkgroupIdentifier talkgroup)
|
||||
{
|
||||
Integer tgValue = talkgroup.getValue();
|
||||
|
||||
for(Map.Entry<Integer,IntegerIdentifier> entry: idMap.entrySet())
|
||||
{
|
||||
if(tgValue != null && tgValue.equals(entry.getValue().getValue()))
|
||||
{
|
||||
DMRLsn lsn = lsnMap.get(entry.getKey());
|
||||
setCurrentChannel(lsn);
|
||||
return lsn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void broadcast(IDecodeEvent event)
|
||||
{
|
||||
super.broadcast(event);
|
||||
|
||||
if(mChannel.isTrafficChannel() && hasTrafficChannelManager())
|
||||
{
|
||||
mTrafficChannelManager.receiveTrafficChannelEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message is valid or if the Ignore CRC Checksums feature is enabled.
|
||||
* @param message to check
|
||||
* @return true if ignore CRC checksums or if the message is valid, meaning the message has passed CRC check.
|
||||
*/
|
||||
private boolean isValid(IMessage message)
|
||||
{
|
||||
return mIgnoreCRCChecksums || message.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this decoder state has an (optional) traffic channel manager.
|
||||
*/
|
||||
@@ -149,6 +230,16 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
return mTrafficChannelManager != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial call event for a traffic channel.
|
||||
* @param decodeEvent to use as the initial call event.
|
||||
*/
|
||||
public void setCurrentCallEvent(DecodeEvent decodeEvent)
|
||||
{
|
||||
mCurrentCallEvent = decodeEvent;
|
||||
setCurrentChannel(decodeEvent.getChannelDescriptor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes channel configuration change notifications received over the processing chain event bus. This is
|
||||
* primarily used for Capacity+ systems when the standard channel is converted to a traffic channel. In response,
|
||||
@@ -185,7 +276,6 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
resetState();
|
||||
|
||||
mCurrentFrequency = 0;
|
||||
mDetectedCallEventsMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,21 +295,21 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
if(message.getTimeslot() == getTimeslot())
|
||||
{
|
||||
if(message instanceof VoiceMessage)
|
||||
if(message instanceof VoiceMessage voice)
|
||||
{
|
||||
processVoice((VoiceMessage)message);
|
||||
processVoice(voice);
|
||||
}
|
||||
else if(message instanceof DataMessage)
|
||||
else if(message instanceof DataMessage data)
|
||||
{
|
||||
processData((DataMessage)message);
|
||||
processData(data);
|
||||
}
|
||||
else if((message.isValid() || mIgnoreCRCChecksums) && message instanceof LCMessage)
|
||||
else if(isValid(message) && message instanceof LCMessage lcMessage)
|
||||
{
|
||||
processLinkControl((LCMessage)message, false);
|
||||
processLinkControl(lcMessage, false);
|
||||
}
|
||||
else if(message.isValid() && message instanceof DMRPacketMessage)
|
||||
else if(isValid(message) && message instanceof DMRPacketMessage packet)
|
||||
{
|
||||
processPacket((DMRPacketMessage)message);
|
||||
processPacket(packet);
|
||||
}
|
||||
else if(message instanceof UDTShortMessageService sms)
|
||||
{
|
||||
@@ -231,16 +321,15 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
}
|
||||
//SLCO messages on timeslot 0 to catch capacity plus rest channel events
|
||||
else if((message.isValid() || mIgnoreCRCChecksums) && message.getTimeslot() == 0 && message instanceof LCMessage)
|
||||
else if(isValid(message) && message.getTimeslot() == 0 && message instanceof LCMessage lcMessage)
|
||||
{
|
||||
processLinkControl((LCMessage)message, false);
|
||||
processLinkControl(lcMessage, false);
|
||||
}
|
||||
|
||||
//Pass the message to the network configuration monitor, if this decoder state has a non-null instance
|
||||
if(mNetworkConfigurationMonitor != null && (message.isValid() || mIgnoreCRCChecksums) &&
|
||||
message instanceof DMRMessage)
|
||||
if(mNetworkConfigurationMonitor != null && isValid(message) && message instanceof DMRMessage dmrMessage)
|
||||
{
|
||||
mNetworkConfigurationMonitor.process((DMRMessage)message);
|
||||
mNetworkConfigurationMonitor.process(dmrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +385,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
broadcast(new DecoderStateEvent(this, Event.START, State.DATA, getTimeslot()));
|
||||
|
||||
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.details("MESSAGE: " + sms.getSMS())
|
||||
.identifiers(new IdentifierCollection(sms.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
@@ -320,6 +410,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
mic.update(hyteraSmsPacket.getDestination());
|
||||
|
||||
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details("SMS:" + hyteraSmsPacket.getSMS())
|
||||
@@ -335,6 +426,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
sb.append("HYTERA RRS REGISTER RADIO:");
|
||||
sb.append(rrs.getDestination());
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(sb.toString())
|
||||
@@ -360,6 +452,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(sb.toString())
|
||||
@@ -372,6 +465,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
|
||||
@@ -384,6 +478,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(ars.toString())
|
||||
@@ -396,6 +491,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(lrrp.toString())
|
||||
@@ -408,6 +504,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(xcmp.toString())
|
||||
@@ -417,6 +514,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
else
|
||||
{
|
||||
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.details(packet.toString())
|
||||
@@ -445,11 +543,18 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
*/
|
||||
private void processVoice(VoiceMessage message)
|
||||
{
|
||||
if(message.getSyncPattern().isDirectMode())
|
||||
if(message.getSyncPattern().isMobileSyncPattern())
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||
if(message.getSyncPattern().isDirectMode())
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "REPEATER", message.getTimestamp());
|
||||
}
|
||||
|
||||
//Use the timeslot as the talkgroup identifier since DCDM mode doesn't use talkgroups
|
||||
//Use the timeslot as the talkgroup identifier since DCDM & simple repeater modes don't use talkgroups
|
||||
getIdentifierCollection().update(DMRTalkgroup.create(getTimeslot()));
|
||||
}
|
||||
else
|
||||
@@ -495,7 +600,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
//Process the link control message to get the identifiers
|
||||
LCMessage lc = header.getLCMessage();
|
||||
|
||||
if(lc.isValid())
|
||||
if(isValid(lc))
|
||||
{
|
||||
processLinkControl(lc, false);
|
||||
}
|
||||
@@ -512,15 +617,15 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
switch(message.getSlotType().getDataType())
|
||||
{
|
||||
case CSBK:
|
||||
if((message.isValid() || mIgnoreCRCChecksums) && message instanceof CSBKMessage)
|
||||
if(isValid(message) && message instanceof CSBKMessage csbk)
|
||||
{
|
||||
processCSBK((CSBKMessage)message);
|
||||
processCSBK(csbk);
|
||||
}
|
||||
break;
|
||||
case VOICE_HEADER:
|
||||
if(message instanceof HeaderMessage)
|
||||
if(message instanceof HeaderMessage header)
|
||||
{
|
||||
processVoiceHeader((HeaderMessage)message);
|
||||
processVoiceHeader(header);
|
||||
}
|
||||
break;
|
||||
case USB_DATA:
|
||||
@@ -531,9 +636,9 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case MBC_ENC_HEADER:
|
||||
case DATA_ENC_HEADER:
|
||||
case CHANNEL_CONTROL_ENC_HEADER:
|
||||
if(message instanceof HeaderMessage)
|
||||
if(message instanceof HeaderMessage header)
|
||||
{
|
||||
processHeader((HeaderMessage)message);
|
||||
processHeader(header);
|
||||
}
|
||||
break;
|
||||
case SLOT_IDLE:
|
||||
@@ -571,7 +676,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
LCMessage lcMessage = terminator.getLCMessage();
|
||||
|
||||
if(lcMessage.isValid())
|
||||
if(isValid(lcMessage))
|
||||
{
|
||||
processLinkControl(lcMessage, true);
|
||||
}
|
||||
@@ -584,7 +689,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
LCMessage lcMessage = voiceHeader.getLCMessage();
|
||||
|
||||
if(lcMessage.isValid())
|
||||
if(isValid(lcMessage))
|
||||
{
|
||||
processLinkControl(lcMessage, false);
|
||||
}
|
||||
@@ -701,7 +806,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case HYTERA_08_ANNOUNCEMENT:
|
||||
case HYTERA_68_ANNOUNCEMENT:
|
||||
case HYTERA_68_XPT_SITE_STATE:
|
||||
|
||||
break;
|
||||
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||
if(csbk instanceof HyteraTrafficChannelTalkerStatus status)
|
||||
{
|
||||
if(status.isChannelActive())
|
||||
{
|
||||
getIdentifierCollection().update(status.getIdentifiers());
|
||||
updateCurrentCall(DecodeEventType.CALL_GROUP, "HYTERA TIER 3 CALL", status.getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(status.getDestinationRadio());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
|
||||
if(csbk instanceof CapacityPlusNeighbors)
|
||||
{
|
||||
@@ -722,20 +842,36 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
//Update state and rest channel
|
||||
updateRestChannel(cpss.getRestChannel());
|
||||
|
||||
//If the sister timeslot hasn't identified the current channel, attempt to identify the channel
|
||||
//from the current call activity map.
|
||||
if(cpss.hasVoiceTalkgroups() && mSisterDecoderState != null && mSisterDecoderState.getCurrentChannel() == null)
|
||||
{
|
||||
DMRChannel sisterChannel = mSisterDecoderState
|
||||
.processActiveTalkgroups(cpss.getActiveIdentifierMap(), cpss.getActiveLsnMap());
|
||||
|
||||
//If the returned channel is non-null, set our own channel
|
||||
if(getCurrentChannel() == null && sisterChannel != null)
|
||||
{
|
||||
setCurrentChannel(sisterChannel.getSisterTimeslot());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_NEIGHBOR_REPORT:
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT:
|
||||
case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM:
|
||||
case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM:
|
||||
case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM:
|
||||
if(csbk instanceof ChannelGrant)
|
||||
case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
if(csbk instanceof ChannelGrant channelGrant)
|
||||
{
|
||||
ChannelGrant dataGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = dataGrant.getChannel();
|
||||
|
||||
DMRChannel channel = channelGrant.getChannel();
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
@@ -745,89 +881,17 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Update the ending timestamp for the event and rebroadcast
|
||||
event.end(csbk.getTimestamp());
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2,
|
||||
"No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
if(csbk instanceof ChannelGrant)
|
||||
else
|
||||
{
|
||||
ChannelGrant tgGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = tgGrant.getChannel();
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
//Log when a CSBK that is not the Unknown CSBK is processed, to detect when new opcodes are added
|
||||
//that are not ChannelGrant subclass implementations.
|
||||
if(!(csbk instanceof UnknownCSBKMessage))
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Update the ending timestamp for the event and rebroadcast
|
||||
event.end(csbk.getTimestamp());
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT:
|
||||
if(csbk instanceof ChannelGrant)
|
||||
{
|
||||
ChannelGrant channelGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = channelGrant.getChannel();
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Update the ending timestamp for the event and rebroadcast
|
||||
event.end(csbk.getTimestamp());
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
mLog.error("Unrecognized DMR channel grant CSBK ignored: " + csbk.getClass());
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -844,42 +908,113 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT:
|
||||
if(csbk instanceof ConnectPlusDataChannelGrant)
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE:
|
||||
if(csbk instanceof CapacityMaxOpenModeVoiceChannelUpdate update)
|
||||
{
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
if(update.hasTimeslot1())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannelTS1());
|
||||
mic.update(update.getTalkgroupTS1());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannelTS1(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
if(update.hasTimeslot2())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannelTS2());
|
||||
mic.update(update.getTalkgroupTS2());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannelTS2(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2,
|
||||
"No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name());
|
||||
}
|
||||
}
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE:
|
||||
if(csbk instanceof CapacityMaxAdvantageModeVoiceChannelUpdate update)
|
||||
{
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
if(update.hasChannel1Timeslot1())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannel1TS1());
|
||||
mic.update(update.getTalkgroupCH1TS1());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannel1TS1(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
|
||||
if(update.hasChannel1Timeslot2())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannel1TS2());
|
||||
mic.update(update.getTalkgroupCH1TS2());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannel1TS2(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
|
||||
if(update.hasChannel2Timeslot1())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannel2TS1());
|
||||
mic.update(update.getTalkgroupCH2TS1());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannel2TS1(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
|
||||
if(update.hasChannel2Timeslot2())
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
mic.remove(IdentifierClass.USER);
|
||||
mic.update(update.getChannel2TS2());
|
||||
mic.update(update.getTalkgroupCH2TS2());
|
||||
mTrafficChannelManager.processChannelGrant(update.getChannel2TS2(), mic, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2,
|
||||
"No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name());
|
||||
}
|
||||
}
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT:
|
||||
if(csbk instanceof ConnectPlusDataChannelGrant cpdcg)
|
||||
{
|
||||
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
|
||||
DMRChannel channel = cpdcg.getChannel();
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Update the ending timestamp for the event and rebroadcast
|
||||
event.end(csbk.getTimestamp());
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2,
|
||||
"No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name());
|
||||
}
|
||||
}
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
|
||||
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.details("Registration Request")
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
@@ -889,6 +1024,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
|
||||
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.details("Registration Response")
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
@@ -897,41 +1033,27 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER:
|
||||
if(csbk instanceof ConnectPlusVoiceChannelUser)
|
||||
if(csbk instanceof ConnectPlusVoiceChannelUser cpvcu)
|
||||
{
|
||||
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
|
||||
DMRChannel channel = cpvcu.getChannel();
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, detectedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Update the ending timestamp for the event and rebroadcast
|
||||
detectedEvent.end(csbk.getTimestamp());
|
||||
}
|
||||
|
||||
broadcast(detectedEvent);
|
||||
LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2,
|
||||
"No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name());
|
||||
}
|
||||
}
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
|
||||
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.details("TALKGROUP AFFILIATION")
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
@@ -945,18 +1067,9 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
}
|
||||
|
||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel,
|
||||
IdentifierCollection identifierCollection) {
|
||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||
.channel(channel)
|
||||
.details(csbk.getOpcode().getLabel())
|
||||
.identifiers(identifierCollection)
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
|
||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.details(details)
|
||||
@@ -977,82 +1090,6 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
return mic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the event is a stale event, meaning that the event is null, or the event start exceeds the max
|
||||
* valid call duration threshold, or if the event identifiers don't match the current identifiers.
|
||||
*
|
||||
* @param event to check for staleness
|
||||
* @param timestamp to check the event against
|
||||
* @param currentIdentifiers to compare against the event
|
||||
* @return true if the event is stale.
|
||||
*/
|
||||
private boolean isStale(DecodeEvent event, long timestamp, List<Identifier> currentIdentifiers)
|
||||
{
|
||||
if(event == null || (timestamp - event.getTimeStart() > MAX_VALID_CALL_DURATION_MS))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isSameCall(event.getIdentifierCollection(), currentIdentifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the TO/FROM identifiers in the identifier collection match the TO/FROM identifiers in the list
|
||||
* of identifiers.
|
||||
* @param identifierCollection containing TO/FROM identifiers
|
||||
* @param identifiers containing TO/FROM identifiers
|
||||
* @return true if the TO/FROM identifiers in each collection match
|
||||
*/
|
||||
private boolean isSameCall(IdentifierCollection identifierCollection, List<Identifier> identifiers)
|
||||
{
|
||||
IntegerIdentifier to = null;
|
||||
IntegerIdentifier from = null;
|
||||
|
||||
for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.TO))
|
||||
{
|
||||
if(identifier instanceof IntegerIdentifier)
|
||||
{
|
||||
to = (IntegerIdentifier)identifier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.FROM))
|
||||
{
|
||||
if(identifier instanceof IntegerIdentifier)
|
||||
{
|
||||
from = (IntegerIdentifier)identifier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(to == null || from == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean toMatch = false;
|
||||
boolean fromMatch = false;
|
||||
|
||||
for(Identifier identifier: identifiers)
|
||||
{
|
||||
if(identifier.getRole() == Role.TO &&
|
||||
identifier instanceof IntegerIdentifier &&
|
||||
((IntegerIdentifier)identifier).getValue() == to.getValue())
|
||||
{
|
||||
toMatch = true;
|
||||
}
|
||||
else if(identifier.getRole() == Role.FROM &&
|
||||
identifier instanceof IntegerIdentifier &&
|
||||
((IntegerIdentifier)identifier).getValue() == from.getValue())
|
||||
{
|
||||
fromMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
return toMatch & fromMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes Link Control Messages
|
||||
* @param isTerminator set to true when the link control is carried by a terminator
|
||||
@@ -1067,6 +1104,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
mCurrentCallEvent.setDetails(ep.getDetails());
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1081,6 +1119,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(cpgvcu.getTalkgroup());
|
||||
}
|
||||
@@ -1098,6 +1137,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(cpgvcu.getTalkgroup());
|
||||
}
|
||||
@@ -1110,6 +1150,77 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER:
|
||||
if(message instanceof CapacityMaxVoiceChannelUser cmvcu)
|
||||
{
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(cmvcu.getTalkgroup());
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollection().update(message.getIdentifiers());
|
||||
ServiceOptions serviceOptions = cmvcu.getServiceOptions();
|
||||
updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED :
|
||||
DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS:
|
||||
if(message instanceof CapacityMaxTalkerAlias alias)
|
||||
{
|
||||
//If we have a talker alias identifier, append this value.
|
||||
Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
|
||||
if(existing instanceof DmrTalkerAliasIdentifier talkerAlias &&
|
||||
!talkerAlias.equals(alias.getTalkerAliasIdentifier()) &&
|
||||
!talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue()))
|
||||
{
|
||||
//Concatenate the existing talker alias fragment with the base alias value.
|
||||
DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier
|
||||
.create(alias.getTalkerAliasIdentifier().getValue() + talkerAlias.getValue());
|
||||
getIdentifierCollection().update(updated);
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollection().update(alias.getTalkerAliasIdentifier());
|
||||
}
|
||||
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION:
|
||||
if(message instanceof CapacityMaxTalkerAliasContinuation alias)
|
||||
{
|
||||
//If we have a talker alias identifier, append this value.
|
||||
Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
|
||||
if(existing instanceof DmrTalkerAliasIdentifier talkerAlias &&
|
||||
!talkerAlias.equals(alias.getTalkerAliasIdentifier()) &&
|
||||
!talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue()))
|
||||
{
|
||||
//Concatenate the existing talker alias value with the updated continuation fragment.
|
||||
DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier.create(talkerAlias.getValue() +
|
||||
alias.getTalkerAliasIdentifier().getValue());
|
||||
getIdentifierCollection().update(updated);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Temporarily place the continuation fragment alias into the identifier collection.
|
||||
getIdentifierCollection().update(alias.getTalkerAliasIdentifier());
|
||||
}
|
||||
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
|
||||
if(message instanceof CapacityPlusWideAreaVoiceChannelUser)
|
||||
{
|
||||
@@ -1119,6 +1230,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(cpuo4.getTalkgroup());
|
||||
}
|
||||
@@ -1135,6 +1247,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(hgvcu.getTalkgroup());
|
||||
}
|
||||
@@ -1159,6 +1272,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(huuvcu.getTargetRadio());
|
||||
}
|
||||
@@ -1178,6 +1292,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(gvcu.getTalkgroup());
|
||||
}
|
||||
@@ -1197,6 +1312,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(isTerminator)
|
||||
{
|
||||
closeCurrentCallEvent(message.getTimestamp());
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
getIdentifierCollection().update(uuvcu.getTargetRadio());
|
||||
}
|
||||
@@ -1286,8 +1402,6 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
.identifiers(getIdentifierCollection().copyOf())
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1298,9 +1412,10 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf());
|
||||
mCurrentCallEvent.end(timestamp);
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
|
||||
if(type == DecodeEventType.CALL_GROUP_ENCRYPTED || type == DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED)
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, event, State.ENCRYPTED, getTimeslot()));
|
||||
@@ -1324,6 +1439,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
broadcast(mCurrentCallEvent);
|
||||
mCurrentCallEvent = null;
|
||||
}
|
||||
|
||||
getIdentifierCollection().remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1344,16 +1461,13 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
case REQUEST_RESET:
|
||||
resetState();
|
||||
if(mNetworkConfigurationMonitor != null)
|
||||
{
|
||||
mNetworkConfigurationMonitor.reset();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_SOURCE_FREQUENCY:
|
||||
long previous = mCurrentFrequency;
|
||||
mCurrentFrequency = event.getFrequency();
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
mTrafficChannelManager.setCurrentControlFrequency(mCurrentFrequency);
|
||||
mTrafficChannelManager.setCurrentControlFrequency(previous, mCurrentFrequency, mChannel);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -1364,6 +1478,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
super.start();
|
||||
|
||||
//Change the default (45-second) traffic channel timeout to 1 second
|
||||
if(mChannel.isTrafficChannel())
|
||||
{
|
||||
@@ -1375,9 +1491,4 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +25,10 @@ import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
|
||||
@@ -65,12 +67,14 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
private VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor();
|
||||
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
|
||||
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
|
||||
private MBCAssembler mMBCAssembler = new MBCAssembler();
|
||||
private MBCAssembler mMBCAssembler;
|
||||
private PacketSequenceAssembler mPacketSequenceAssembler;
|
||||
private SLCAssembler mSLCAssembler = new SLCAssembler();
|
||||
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
||||
private Listener<IMessage> mMessageListener;
|
||||
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
||||
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
|
||||
private boolean mIgnoreCrcChecksums;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -78,6 +82,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
public DMRMessageProcessor(DecodeConfigDMR config)
|
||||
{
|
||||
mConfigDMR = config;
|
||||
mIgnoreCrcChecksums = config.getIgnoreCRCChecksums();
|
||||
mMBCAssembler = new MBCAssembler(mIgnoreCrcChecksums);
|
||||
|
||||
for(TimeslotFrequency timeslotFrequency: config.getTimeslotMap())
|
||||
{
|
||||
@@ -87,12 +93,34 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
mPacketSequenceAssembler = new PacketSequenceAssembler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message is valid or if the Ignore CRC Checksums feature is enabled.
|
||||
*
|
||||
* @param message to check
|
||||
* @return true if ignore CRC checksums or if the message is valid.
|
||||
*/
|
||||
private boolean isValid(IMessage message)
|
||||
{
|
||||
return mIgnoreCrcChecksums || message.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary message processing
|
||||
*/
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Detect and correct messages employing an alternate CRC mask pattern.
|
||||
if(!message.isValid() && message instanceof CSBKMessage csbk)
|
||||
{
|
||||
mCrcMaskManager.check(csbk);
|
||||
}
|
||||
|
||||
if(message instanceof FullLCMessage flc)
|
||||
{
|
||||
if(flc.getTimeslot() == 1)
|
||||
@@ -115,7 +143,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
mSuperFrameProcessor2.process(voiceMessage);
|
||||
}
|
||||
}
|
||||
else if(message instanceof DMRBurst dmrBurst && dmrBurst.isValid())
|
||||
else if(message instanceof DMRBurst dmrBurst && isValid(dmrBurst))
|
||||
{
|
||||
if(dmrBurst.getTimeslot() == 1)
|
||||
{
|
||||
@@ -127,36 +155,21 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
}
|
||||
}
|
||||
|
||||
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
|
||||
if(message instanceof ITimeslotFrequencyReceiver)
|
||||
//Process data messages carrying a link control payload so that the LC payload can be processed/enriched
|
||||
if(message instanceof DataMessageWithLinkControl linkControlCarrier)
|
||||
{
|
||||
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
|
||||
int[] lcns = receiver.getLogicalChannelNumbers();
|
||||
|
||||
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
|
||||
|
||||
for(int lcn: lcns)
|
||||
{
|
||||
if(mTimeslotFrequencyMap.containsKey(lcn))
|
||||
{
|
||||
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
|
||||
}
|
||||
}
|
||||
|
||||
if(!timeslotFrequencies.isEmpty())
|
||||
{
|
||||
receiver.apply(timeslotFrequencies);
|
||||
}
|
||||
enrich(linkControlCarrier.getLCMessage());
|
||||
}
|
||||
|
||||
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
|
||||
enrich(message);
|
||||
|
||||
//Now that the message has been (potentially) enriched, dispatch it to the modules
|
||||
dispatch(message);
|
||||
|
||||
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
|
||||
if(message instanceof VoiceEMBMessage)
|
||||
if(message instanceof VoiceEMBMessage voice)
|
||||
{
|
||||
VoiceEMBMessage voice = (VoiceEMBMessage)message;
|
||||
|
||||
if(message.getTimeslot() == 1)
|
||||
{
|
||||
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
||||
@@ -232,19 +245,47 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
}
|
||||
|
||||
//Reset talker alias assembler on Idle or Terminator
|
||||
if(message.isValid() && (message instanceof IDLEMessage || message instanceof Terminator))
|
||||
if(isValid(message) && (message instanceof IDLEMessage || message instanceof Terminator))
|
||||
{
|
||||
mTalkerAliasAssembler.reset(message.getTimeslot());
|
||||
}
|
||||
}
|
||||
|
||||
//Assemble Talker Alias from FLC message fragments (header & blocks 1-3)
|
||||
if(message instanceof FullLCMessage flc && flc.getOpcode().isTalkerAliasOpcode() && message.isValid())
|
||||
if(message instanceof FullLCMessage flc && flc.getOpcode().isTalkerAliasOpcode() && isValid(message))
|
||||
{
|
||||
dispatch(mTalkerAliasAssembler.process(flc));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
|
||||
* @param message to be enriched
|
||||
*/
|
||||
private void enrich(IMessage message)
|
||||
{
|
||||
if(message instanceof ITimeslotFrequencyReceiver)
|
||||
{
|
||||
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
|
||||
int[] lcns = receiver.getLogicalChannelNumbers();
|
||||
|
||||
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
|
||||
|
||||
for(int lcn: lcns)
|
||||
{
|
||||
if(mTimeslotFrequencyMap.containsKey(lcn))
|
||||
{
|
||||
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
|
||||
}
|
||||
}
|
||||
|
||||
if(!timeslotFrequencies.isEmpty())
|
||||
{
|
||||
receiver.apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the non-null message to the registered listener
|
||||
*/
|
||||
|
||||
@@ -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.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRSite;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAdjacentSiteInformation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
|
||||
@@ -36,6 +37,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusControlChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ControlChannelSystemParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.TrafficChannelSystemParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Model;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||
import java.util.ArrayList;
|
||||
@@ -59,6 +62,11 @@ public class DMRNetworkConfigurationMonitor
|
||||
private static final String BRAND_TIER_3_TRUNKING = "Tier III Trunking";
|
||||
private static final String BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING = "Capacity Max Tier III Trunking";
|
||||
private static final String BRAND_HYTERA_TIER_3_TRUNKING = "Hytera Tier III Trunking";
|
||||
private static final String MODE_CAPACITY_MAX_OPEN_SYSTEM = "Open System";
|
||||
private static final String MODE_CAPACITY_MAX_ADVANTAGE = "Advantage";
|
||||
private static final String CHANNEL_TYPE_CONTROL = "Control";
|
||||
private static final String CHANNEL_TYPE_TRAFFIC = "Traffic";
|
||||
private static final String UNKNOWN = "Unknown";
|
||||
|
||||
private List<SiteIdentifier> mNeighborSites = new ArrayList<>();
|
||||
private Map<Integer,AdjacentSiteInformation> mTier3NeighborSites = new HashMap<>();
|
||||
@@ -67,7 +75,10 @@ public class DMRNetworkConfigurationMonitor
|
||||
private DMRSite mDMRSite;
|
||||
private Model mTier3Model;
|
||||
private String mBrand;
|
||||
private Integer mColorCode;
|
||||
private String mMode;
|
||||
private String mChannelType;
|
||||
private Integer mColorCodeTS1;
|
||||
private Integer mColorCodeTS2;
|
||||
private DMRChannel mCurrentChannel;
|
||||
private Channel mChannel;
|
||||
|
||||
@@ -94,13 +105,34 @@ public class DMRNetworkConfigurationMonitor
|
||||
*/
|
||||
public void process(DMRMessage message)
|
||||
{
|
||||
if(message instanceof CSBKMessage)
|
||||
if(message instanceof CSBKMessage csbk)
|
||||
{
|
||||
process((CSBKMessage)message);
|
||||
process(csbk);
|
||||
}
|
||||
else if(message instanceof LCMessage)
|
||||
else if(message instanceof LCMessage lc)
|
||||
{
|
||||
process((LCMessage)message);
|
||||
process(lc);
|
||||
}
|
||||
|
||||
if(message instanceof DataMessage dm)
|
||||
{
|
||||
process(dm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes data messages to capture the color code for each timeslot.
|
||||
* @param dm data message
|
||||
*/
|
||||
public void process(DataMessage dm)
|
||||
{
|
||||
if(dm.getTimeslot() == 1)
|
||||
{
|
||||
mColorCodeTS1 = dm.getSlotType().getColorCode();
|
||||
}
|
||||
else if(dm.getTimeslot() == 2)
|
||||
{
|
||||
mColorCodeTS2 = dm.getSlotType().getColorCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,30 +143,59 @@ public class DMRNetworkConfigurationMonitor
|
||||
{
|
||||
switch(linkControl.getOpcode())
|
||||
{
|
||||
case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER:
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS:
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION:
|
||||
mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
|
||||
break;
|
||||
|
||||
case SHORT_CONNECT_PLUS_CONTROL_CHANNEL:
|
||||
if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusControlChannel)
|
||||
if(linkControl instanceof ConnectPlusControlChannel cpcc)
|
||||
{
|
||||
ConnectPlusControlChannel cpcc = (ConnectPlusControlChannel)linkControl;
|
||||
mDMRNetwork = cpcc.getNetwork();
|
||||
mDMRSite = cpcc.getSite();
|
||||
}
|
||||
if(mBrand == null)
|
||||
{
|
||||
mChannelType = CHANNEL_TYPE_CONTROL;
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
}
|
||||
break;
|
||||
case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL:
|
||||
if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusTrafficChannel)
|
||||
if(linkControl instanceof ConnectPlusTrafficChannel cptc)
|
||||
{
|
||||
ConnectPlusTrafficChannel cptc = (ConnectPlusTrafficChannel)linkControl;
|
||||
mDMRNetwork = cptc.getNetwork();
|
||||
mDMRSite = cptc.getSite();
|
||||
}
|
||||
if(mBrand == null)
|
||||
{
|
||||
mChannelType = CHANNEL_TYPE_TRAFFIC;
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
}
|
||||
break;
|
||||
case SHORT_STANDARD_CONTROL_CHANNEL_SYSTEM_PARAMETERS:
|
||||
if(linkControl instanceof ControlChannelSystemParameters cc)
|
||||
{
|
||||
SystemIdentityCode sic = cc.getSystemIdentityCode();
|
||||
mTier3Model = sic.getModel();
|
||||
mDMRNetwork = sic.getNetwork();
|
||||
mDMRSite = sic.getSite();
|
||||
mChannelType = CHANNEL_TYPE_CONTROL;
|
||||
|
||||
if(mBrand == null)
|
||||
{
|
||||
mBrand = BRAND_TIER_3_TRUNKING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SHORT_STANDARD_TRAFFIC_CHANNEL_SYSTEM_PARAMETERS:
|
||||
if(linkControl instanceof TrafficChannelSystemParameters tc)
|
||||
{
|
||||
SystemIdentityCode sic = tc.getSystemIdentityCode();
|
||||
mTier3Model = sic.getModel();
|
||||
mDMRNetwork = sic.getNetwork();
|
||||
mDMRSite = sic.getSite();
|
||||
mChannelType = CHANNEL_TYPE_TRAFFIC;
|
||||
}
|
||||
|
||||
if(mBrand == null)
|
||||
{
|
||||
mBrand = BRAND_TIER_3_TRUNKING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +233,12 @@ public class DMRNetworkConfigurationMonitor
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STANDARD_ANNOUNCEMENT:
|
||||
if(csbk instanceof AdjacentSiteInformation neighbor)
|
||||
{
|
||||
mTier3NeighborSites.put(neighbor.getNeighborSystemIdentityCode().getSite().getValue(), neighbor);
|
||||
}
|
||||
break;
|
||||
case HYTERA_08_ANNOUNCEMENT:
|
||||
case HYTERA_68_ANNOUNCEMENT:
|
||||
if(csbk instanceof HyteraAnnouncement)
|
||||
@@ -196,10 +263,7 @@ public class DMRNetworkConfigurationMonitor
|
||||
mTier3Model = ha.getSystemIdentityCode().getModel();
|
||||
}
|
||||
|
||||
if(mBrand == null || mBrand != BRAND_HYTERA_TIER_3_TRUNKING)
|
||||
{
|
||||
mBrand = BRAND_HYTERA_TIER_3_TRUNKING;
|
||||
}
|
||||
mBrand = BRAND_HYTERA_TIER_3_TRUNKING;
|
||||
}
|
||||
if(csbk instanceof HyteraAdjacentSiteInformation)
|
||||
{
|
||||
@@ -234,22 +298,25 @@ public class DMRNetworkConfigurationMonitor
|
||||
}
|
||||
}
|
||||
|
||||
if(mBrand == null || mBrand != BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING)
|
||||
{
|
||||
mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
|
||||
}
|
||||
mChannelType = CHANNEL_TYPE_CONTROL;
|
||||
mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
|
||||
}
|
||||
break;
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE:
|
||||
mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
|
||||
mMode = MODE_CAPACITY_MAX_ADVANTAGE;
|
||||
break;
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE:
|
||||
mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
|
||||
mMode = MODE_CAPACITY_MAX_OPEN_SYSTEM;
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_NEIGHBOR_REPORT:
|
||||
if(mNeighborSites.isEmpty() && csbk instanceof ConnectPlusNeighborReport)
|
||||
{
|
||||
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
|
||||
mNeighborSites.addAll(cpnr.getNeighbors());
|
||||
}
|
||||
if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
|
||||
{
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
}
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER:
|
||||
if(csbk instanceof ConnectPlusVoiceChannelUser)
|
||||
@@ -257,17 +324,9 @@ public class DMRNetworkConfigurationMonitor
|
||||
DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel();
|
||||
addDmrChannel(channel);
|
||||
}
|
||||
if(mBrand == null)
|
||||
{
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
}
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
break;
|
||||
}
|
||||
|
||||
if(mColorCode == null)
|
||||
{
|
||||
mColorCode = csbk.getSlotType().getColorCode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,33 +337,31 @@ public class DMRNetworkConfigurationMonitor
|
||||
mObservedChannelMap.put(dmrChannel.getValue(), dmrChannel);
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mColorCode = null;
|
||||
mDMRNetwork = null;
|
||||
mDMRSite = null;
|
||||
mNeighborSites.clear();
|
||||
mTier3NeighborSites.clear();
|
||||
mObservedChannelMap.clear();
|
||||
}
|
||||
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Activity Summary - Decoder: DMR ");
|
||||
|
||||
//DMR System Brand Name
|
||||
sb.append("\n\nBrand:").append((mBrand == null ? BRAND_STANDARD : mBrand));
|
||||
sb.append("\n\nBrand: ").append((mBrand == null ? BRAND_STANDARD : mBrand));
|
||||
|
||||
if(mMode != null)
|
||||
{
|
||||
sb.append("\nConfigured Mode: ").append(mMode);
|
||||
}
|
||||
|
||||
sb.append("\nChannel Type: ").append((mChannelType != null) ? mChannelType : UNKNOWN);
|
||||
sb.append("\nColor Code Timeslot 1: ").append(mColorCodeTS1 != null ? mColorCodeTS1 : UNKNOWN);
|
||||
sb.append("\nColor Code Timeslot 2: ").append(mColorCodeTS2 != null ? mColorCodeTS2 : UNKNOWN);
|
||||
|
||||
if(mDMRNetwork != null)
|
||||
{
|
||||
sb.append("\nNetwork:").append(mDMRNetwork);
|
||||
sb.append("\nNetwork: ").append(mDMRNetwork);
|
||||
}
|
||||
|
||||
if(mDMRSite != null)
|
||||
{
|
||||
sb.append("\nSite:").append(mDMRSite);
|
||||
sb.append("\nSite: ").append(mDMRSite);
|
||||
}
|
||||
|
||||
if(mTier3Model != null)
|
||||
@@ -347,6 +404,11 @@ public class DMRNetworkConfigurationMonitor
|
||||
{
|
||||
sb.append("\tNeighbor: ").append(neighbor).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if(!mTier3NeighborSites.isEmpty())
|
||||
{
|
||||
sb.append("\nTier III Neighbor Sites\n");
|
||||
|
||||
for(Map.Entry<Integer,AdjacentSiteInformation> neighbor: mTier3NeighborSites.entrySet())
|
||||
{
|
||||
@@ -354,7 +416,16 @@ public class DMRNetworkConfigurationMonitor
|
||||
sb.append("\tNeighbor: ").append("Network:").append(site.getNeighborSystemIdentityCode().getNetwork());
|
||||
sb.append(" Site:").append(site.getNeighborSystemIdentityCode().getSite());
|
||||
sb.append(" ").append(site.getNeighborSystemIdentityCode().getModel());
|
||||
sb.append(" ").append(site.getNeighborChannel()).append("\n");
|
||||
|
||||
DMRChannel channel = site.getNeighborChannel();
|
||||
sb.append(" ").append(channel);
|
||||
|
||||
if(channel.getDownlinkFrequency() > 0)
|
||||
{
|
||||
sb.append(" ").append(channel.getDownlinkFrequency());
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -91,9 +91,17 @@ public enum DMRSyncPattern
|
||||
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
||||
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
||||
|
||||
//Direct Mode Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
|
||||
DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
|
||||
//Direct Mode TS 1 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
|
||||
DIRECT_MODE_DATA_TIMESLOT_1);
|
||||
|
||||
//Direct Mode TS 2 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS2_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_2,
|
||||
DIRECT_MODE_DATA_TIMESLOT_2);
|
||||
|
||||
//Mobile Station Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> MOBILE_STATION_PATTERNS = EnumSet.of(MOBILE_STATION_DATA,
|
||||
MOBILE_STATION_VOICE, MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F);
|
||||
|
||||
/**
|
||||
* Pattern that represents the enum entry
|
||||
@@ -140,7 +148,43 @@ public enum DMRSyncPattern
|
||||
*/
|
||||
public boolean isDirectMode()
|
||||
{
|
||||
return DIRECT_MODE_PATTERNS.contains(this);
|
||||
return isDirectModeTS1() || isDirectModeTS2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 1
|
||||
* @return true if DMTS1
|
||||
*/
|
||||
public boolean isDirectModeTS1()
|
||||
{
|
||||
return DIRECT_MODE_TS1_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 2
|
||||
* @return true if DMTS2
|
||||
*/
|
||||
public boolean isDirectModeTS2()
|
||||
{
|
||||
return DIRECT_MODE_TS2_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile station sync pattern
|
||||
* @return true if a mobile station sync pattern.
|
||||
*/
|
||||
public boolean isMobileStationSyncPattern()
|
||||
{
|
||||
return MOBILE_STATION_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile sync pattern, either mobile station or direct mode.
|
||||
* @return true if a mobile sync pattern.
|
||||
*/
|
||||
public boolean isMobileSyncPattern()
|
||||
{
|
||||
return isMobileStationSyncPattern() || isDirectMode();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,7 +37,7 @@ import io.github.dsheirer.message.MessageHistoryRequest;
|
||||
import io.github.dsheirer.message.MessageHistoryResponse;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.event.DMRChannelGrantEvent;
|
||||
import io.github.dsheirer.module.decode.dmr.event.DMRDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
@@ -63,6 +63,7 @@ import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -83,36 +84,32 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* This manager monitors channel events watching for events related to managed traffic channels.
|
||||
*/
|
||||
public class DMRTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener,
|
||||
IChannelEventProvider
|
||||
public class DMRTrafficChannelManager extends TrafficChannelManager implements IChannelEventListener,
|
||||
IChannelEventProvider, IDecodeEventProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRTrafficChannelManager.class);
|
||||
public static final String CHANNEL_START_REJECTED = " REJECTED - NO TUNER";
|
||||
public static final String DATA_CALL_IGNORED = "DATA CALL IGNORED";
|
||||
public static final String IGNORED = " - Ignored";
|
||||
public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED";
|
||||
public static final String NO_FREQUENCY = "NO FREQUENCY - CHECK LSN CHANNEL MAP";
|
||||
public static final String NO_FREQUENCY = "NO FREQUENCY - CHECK PLAYLIST CHANNEL CONFIG LSN CHANNEL MAP";
|
||||
public static final long EVENT_TIME_STALE_THRESHOLD = 5000; //5 seconds
|
||||
|
||||
private Queue<Channel> mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>();
|
||||
private List<Channel> mManagedTrafficChannels;
|
||||
|
||||
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
|
||||
private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
|
||||
|
||||
private Queue<Channel> mAvailableTrafficChannels = new ConcurrentLinkedQueue<>();
|
||||
private List<Channel> mAllocatedTrafficChannels;
|
||||
private Map<Long,Channel> mAllocatedChannelFrequencyMap = new ConcurrentHashMap<>();
|
||||
private ReentrantLock mLock = new ReentrantLock();
|
||||
private Map<Long,IDecodeEvent> mCallEventsTS1 = new ConcurrentHashMap<>();
|
||||
private Map<Long,IDecodeEvent> mCallEventsTS2 = new ConcurrentHashMap<>();
|
||||
private Listener<ChannelEvent> mChannelEventListener;
|
||||
private Listener<IDecodeEvent> mDecodeEventListener;
|
||||
|
||||
private TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor();
|
||||
private Channel mParentChannel;
|
||||
private boolean mIgnoreDataCalls;
|
||||
|
||||
//Used as temporary storage for message and decode event history during Cap+ REST channel rotation
|
||||
private DecodeEventHistory mTransientDecodeEventHistory;
|
||||
private List<IMessage> mTransientMessageHistory;
|
||||
|
||||
private boolean mIgnoreDataCalls;
|
||||
private long mCurrentControlFrequency;
|
||||
|
||||
/**
|
||||
* Monitors call events and allocates traffic decoder channels in response
|
||||
* to traffic channel allocation call events. Manages a pool of reusable
|
||||
@@ -135,11 +132,38 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
/**
|
||||
* Sets the current parent control channel frequency so that channel grants for the current frequency do not
|
||||
* produce an additional traffic channel allocation.
|
||||
* @param currentControlFrequency for current control channel.
|
||||
* @param previousControlFrequency for the current control channel (to remove from allocated channels)
|
||||
* @param currentControlFrequency for current control channel (to add to allocated channels)
|
||||
* @param channel for the current control channel
|
||||
*/
|
||||
public void setCurrentControlFrequency(long currentControlFrequency)
|
||||
public void setCurrentControlFrequency(long previousControlFrequency, long currentControlFrequency, Channel channel)
|
||||
{
|
||||
mCurrentControlFrequency = currentControlFrequency;
|
||||
if(previousControlFrequency == currentControlFrequency)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
Channel existing = mAllocatedChannelFrequencyMap.get(previousControlFrequency);
|
||||
|
||||
//Only remove the channel if it is non-null and it matches the current control channel.
|
||||
if(channel.equals(existing))
|
||||
{
|
||||
//Unlock the frequency in the channel rotation monitor
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(previousControlFrequency));
|
||||
mAllocatedChannelFrequencyMap.remove(previousControlFrequency);
|
||||
}
|
||||
|
||||
mAllocatedChannelFrequencyMap.put(currentControlFrequency, channel);
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(currentControlFrequency));
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,7 +173,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
*/
|
||||
private void createTrafficChannels()
|
||||
{
|
||||
if(mManagedTrafficChannels == null)
|
||||
if(mAllocatedTrafficChannels == null)
|
||||
{
|
||||
DecodeConfiguration decodeConfiguration = mParentChannel.getDecodeConfiguration();
|
||||
List<Channel> trafficChannelList = new ArrayList<>();
|
||||
@@ -176,8 +200,11 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
}
|
||||
}
|
||||
|
||||
mAvailableTrafficChannelQueue.addAll(trafficChannelList);
|
||||
mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList);
|
||||
mAvailableTrafficChannels.addAll(trafficChannelList);
|
||||
|
||||
//Keep track of the complete list so that we can check channel events to determine if the channel event
|
||||
//is for a traffic channel owned by this traffic channel manager.
|
||||
mAllocatedTrafficChannels = Collections.unmodifiableList(trafficChannelList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,85 +218,93 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
public void convertToTrafficChannel(Channel channel, long currentFrequency, IChannelDescriptor restChannel,
|
||||
DMRNetworkConfigurationMonitor networkConfigurationMonitor)
|
||||
{
|
||||
//Only do the conversion of the original channel has multiple frequencies defined and the rest channel is
|
||||
//one of those frequencies
|
||||
if(restChannel.getDownlinkFrequency() > 0 &&
|
||||
!mAllocatedTrafficChannelFrequencyMap.containsKey(restChannel.getDownlinkFrequency()) &&
|
||||
channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES)
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration();
|
||||
long rest = restChannel.getDownlinkFrequency();
|
||||
|
||||
//Add the rest channel to the list of frequencies in the source configuration
|
||||
if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency()))
|
||||
//Only do the conversion of the original channel has multiple frequencies defined and the rest channel is
|
||||
//one of those frequencies
|
||||
if(rest > 0 && !mAllocatedChannelFrequencyMap.containsKey(rest) &&
|
||||
channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES)
|
||||
{
|
||||
originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency());
|
||||
SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration();
|
||||
|
||||
//Note: the channel configuration modification is not persisted from here, to avoid unnecessary coupling
|
||||
//to the channel model. The configuration will simply be adjusted at runtime.
|
||||
}
|
||||
|
||||
//Convert current channel to a traffic channel if one is available
|
||||
Channel trafficChannel = mAvailableTrafficChannelQueue.poll();
|
||||
|
||||
if(trafficChannel != null && restChannel.getDownlinkFrequency() > 0)
|
||||
{
|
||||
//Disable the channel rotation manager when we have multiple frequencies defined
|
||||
getInterModuleEventBus().post(new DisableChannelRotationMonitorRequest());
|
||||
|
||||
//Set the frequency for the traffic channel configuration
|
||||
SourceConfigTuner trafficSourceConfig = new SourceConfigTuner();
|
||||
trafficSourceConfig.setFrequency(currentFrequency);
|
||||
trafficChannel.setSourceConfiguration(trafficSourceConfig);
|
||||
|
||||
//Post a request for message and decode event history to transfer to the new REST channel. This has
|
||||
// to be posted before the channel conversion request
|
||||
getInterModuleEventBus().post(new DecodeEventHistoryRequest());
|
||||
getInterModuleEventBus().post(new MessageHistoryRequest());
|
||||
|
||||
//Dispatch a request to convert this processing chain to the traffic channel. This will cause the
|
||||
//processing chain to convert to a traffic channel and notify all of the modules, which will in-turn
|
||||
//cause the decoder states (2 timeslots) to dereference this manager so that the existing channel can
|
||||
//no longer allocate traffic channels.
|
||||
getInterModuleEventBus().post(new ChannelConversionRequest(channel, trafficChannel));
|
||||
|
||||
mAllocatedTrafficChannelFrequencyMap.put(currentFrequency, trafficChannel);
|
||||
|
||||
//Set the preferred frequency to use when restarting the original channel
|
||||
originalSourceConfig.setPreferredFrequency(restChannel.getDownlinkFrequency());
|
||||
|
||||
//Dispatch request to persistently start the original channel with the rest channel frequency and reuse
|
||||
//this traffic channel manager in the new processing chain.
|
||||
ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(channel, this);
|
||||
request.setPersistentAttempt(true);
|
||||
request.setChildDecodeEventHistory(mTransientDecodeEventHistory);
|
||||
|
||||
//If we received an event history response, add it to the request as preload data content
|
||||
if(mTransientDecodeEventHistory != null)
|
||||
{
|
||||
DecodeEventHistoryPreloadData eventHistory =
|
||||
new DecodeEventHistoryPreloadData(mTransientDecodeEventHistory.getItems());
|
||||
request.addPreloadDataContent(eventHistory);
|
||||
|
||||
mTransientDecodeEventHistory = null;
|
||||
}
|
||||
|
||||
//If we received a message history response, add it to the request as preload data content
|
||||
if(mTransientMessageHistory != null)
|
||||
{
|
||||
MessageHistoryPreloadData messageHistory = new MessageHistoryPreloadData(mTransientMessageHistory);
|
||||
request.addPreloadDataContent(messageHistory);
|
||||
mTransientMessageHistory = null;
|
||||
}
|
||||
|
||||
//Add the DMR network configuration monitor as preload data
|
||||
if(networkConfigurationMonitor != null)
|
||||
{
|
||||
request.addPreloadDataContent(new DMRNetworkConfigurationPreloadData(networkConfigurationMonitor));
|
||||
}
|
||||
|
||||
getInterModuleEventBus().post(request);
|
||||
//Add the rest channel to the list of frequencies in the source configuration
|
||||
if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency()))
|
||||
{
|
||||
originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency());
|
||||
}
|
||||
|
||||
//Convert current channel to a traffic channel if one is available
|
||||
Channel trafficChannel = mAvailableTrafficChannels.poll();
|
||||
|
||||
if(trafficChannel != null)
|
||||
{
|
||||
//Disable the channel rotation manager when we have multiple frequencies defined
|
||||
getInterModuleEventBus().post(new DisableChannelRotationMonitorRequest());
|
||||
|
||||
//Set the frequency for the traffic channel configuration
|
||||
SourceConfigTuner trafficSourceConfig = new SourceConfigTuner();
|
||||
trafficSourceConfig.setFrequency(currentFrequency);
|
||||
trafficChannel.setSourceConfiguration(trafficSourceConfig);
|
||||
|
||||
//Post a request for message and decode event history to transfer to the new REST channel. This has
|
||||
// to be posted before the channel conversion request
|
||||
getInterModuleEventBus().post(new DecodeEventHistoryRequest());
|
||||
getInterModuleEventBus().post(new MessageHistoryRequest());
|
||||
|
||||
//Dispatch a request to convert this processing chain to the traffic channel. This will cause the
|
||||
//processing chain to convert to a traffic channel and notify all of the modules, which will in-turn
|
||||
//cause the decoder states (2 timeslots) to dereference this manager so that the existing channel can
|
||||
//no longer allocate traffic channels.
|
||||
getInterModuleEventBus().post(new ChannelConversionRequest(channel, trafficChannel));
|
||||
|
||||
mAllocatedChannelFrequencyMap.put(currentFrequency, trafficChannel);
|
||||
|
||||
//Set the preferred frequency to use when restarting the original channel
|
||||
originalSourceConfig.setPreferredFrequency(rest);
|
||||
|
||||
//Dispatch request to persistently start the original channel with the rest channel frequency and reuse
|
||||
//this traffic channel manager in the new processing chain.
|
||||
ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(channel, restChannel,
|
||||
null, this);
|
||||
request.setPersistentAttempt(true);
|
||||
request.setChildDecodeEventHistory(mTransientDecodeEventHistory);
|
||||
|
||||
//If we received an event history response, add it to the request as preload data content
|
||||
if(mTransientDecodeEventHistory != null)
|
||||
{
|
||||
DecodeEventHistoryPreloadData eventHistory =
|
||||
new DecodeEventHistoryPreloadData(mTransientDecodeEventHistory.getItems());
|
||||
request.addPreloadDataContent(eventHistory);
|
||||
|
||||
mTransientDecodeEventHistory = null;
|
||||
}
|
||||
|
||||
//If we received a message history response, add it to the request as preload data content
|
||||
if(mTransientMessageHistory != null)
|
||||
{
|
||||
MessageHistoryPreloadData messageHistory = new MessageHistoryPreloadData(mTransientMessageHistory);
|
||||
request.addPreloadDataContent(messageHistory);
|
||||
mTransientMessageHistory = null;
|
||||
}
|
||||
|
||||
//Add the DMR network configuration monitor as preload data
|
||||
if(networkConfigurationMonitor != null)
|
||||
{
|
||||
request.addPreloadDataContent(new DMRNetworkConfigurationPreloadData(networkConfigurationMonitor));
|
||||
}
|
||||
|
||||
getInterModuleEventBus().post(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,7 +336,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
/**
|
||||
* Broadcasts an initial or update decode event to any registered listener.
|
||||
*/
|
||||
public void broadcast(DecodeEvent decodeEvent)
|
||||
public void broadcast(IDecodeEvent decodeEvent)
|
||||
{
|
||||
if(mDecodeEventListener != null)
|
||||
{
|
||||
@@ -309,119 +344,159 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
}
|
||||
}
|
||||
|
||||
public void processEndChannelGrant()
|
||||
/**
|
||||
* Receive decode events/updates from traffic channels to capture in the parent channel's decode event history. We
|
||||
* do this by rebroadcasting the event to the parent control channel's processing chain so that the event history
|
||||
* can receive the event or update.
|
||||
* @param trafficChannelEvent to receive and rebroadcast.
|
||||
*/
|
||||
public void receiveTrafficChannelEvent(IDecodeEvent trafficChannelEvent)
|
||||
{
|
||||
//TODO: process the CSBK opcode that indicates traffic channel teardown
|
||||
broadcast(trafficChannelEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes channel grants to allocate traffic channels and track overall channel usage. Generates
|
||||
* decode events for each new channel that is allocated.
|
||||
*
|
||||
*/
|
||||
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
|
||||
Opcode opcode, long timestamp, boolean encrypted)
|
||||
{
|
||||
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
||||
mLock.lock();
|
||||
|
||||
if(isStale(event, timestamp, identifierCollection)) //Create new event
|
||||
try
|
||||
{
|
||||
event = DMRChannelGrantEvent.channelGrantBuilder(decodeEventType, timestamp)
|
||||
.channel(channel)
|
||||
.details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : ""))
|
||||
.identifiers(identifierCollection)
|
||||
.build();
|
||||
boolean allocated = mAllocatedChannelFrequencyMap.containsKey(channel.getDownlinkFrequency());
|
||||
|
||||
mChannelGrantEventMap.put(channel, event);
|
||||
|
||||
}
|
||||
else //Update current event
|
||||
{
|
||||
Identifier from = getIdentifier(identifierCollection, Role.FROM);
|
||||
|
||||
if(from != null)
|
||||
if(allocated)
|
||||
{
|
||||
Identifier currentFrom = getIdentifier(event.getIdentifierCollection(), Role.FROM);
|
||||
if(currentFrom != null && !Objects.equals(from, currentFrom))
|
||||
{
|
||||
event.end(timestamp);
|
||||
|
||||
event = DMRChannelGrantEvent.channelGrantBuilder(decodeEventType, timestamp)
|
||||
.channel(channel)
|
||||
.details("CONTINUE - CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : ""))
|
||||
.identifiers(identifierCollection)
|
||||
.build();
|
||||
|
||||
mChannelGrantEventMap.put(channel, event);
|
||||
broadcast(event);
|
||||
}
|
||||
}
|
||||
|
||||
//update the ending timestamp so that the duration value is correctly calculated
|
||||
event.update(timestamp);
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
|
||||
//Even though we have an event, the initial or continuation channel grant may have been rejected. Check to see
|
||||
// if there is a traffic channel allocated. If not, allocate one and update the event description.
|
||||
long frequency = channel.getDownlinkFrequency();
|
||||
|
||||
if(frequency == 0)
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(NO_FREQUENCY);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(NO_FREQUENCY))
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + NO_FREQUENCY);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(frequency != mCurrentControlFrequency && !mAllocatedTrafficChannelFrequencyMap.containsKey(frequency))
|
||||
{
|
||||
if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode())
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(DATA_CALL_IGNORED);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(DATA_CALL_IGNORED))
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + DATA_CALL_IGNORED);
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
return;
|
||||
}
|
||||
|
||||
Channel trafficChannel = mAvailableTrafficChannelQueue.poll();
|
||||
|
||||
if(trafficChannel != null)
|
||||
{
|
||||
SourceConfigTuner sourceConfig = new SourceConfigTuner();
|
||||
sourceConfig.setFrequency(frequency);
|
||||
trafficChannel.setSourceConfiguration(sourceConfig);
|
||||
mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel);
|
||||
getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel,
|
||||
identifierCollection));
|
||||
//Note: if traffic channel is allocated then do nothing & let the channel maintain its own event state
|
||||
}
|
||||
else
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
long frequency = channel.getDownlinkFrequency();
|
||||
|
||||
//If we don't have a frequency value for the channel, use the channel number as a place holder. We won't
|
||||
//allocate a traffic channel for it, but we can track the related channel grant event.
|
||||
if(frequency == 0)
|
||||
{
|
||||
event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED);
|
||||
frequency = channel.getChannelNumber();
|
||||
}
|
||||
else if(!event.getDetails().endsWith(MAX_TRAFFIC_CHANNELS_EXCEEDED))
|
||||
|
||||
Map<Long,IDecodeEvent> eventMap = channel.getTimeslot() == 1 ? mCallEventsTS1 : mCallEventsTS2;
|
||||
IDecodeEvent temp = eventMap.get(frequency);
|
||||
DecodeEvent event = (temp != null) ? (DecodeEvent)temp : null;
|
||||
|
||||
if(isStale(event, timestamp, identifierCollection)) //Create new event
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + MAX_TRAFFIC_CHANNELS_EXCEEDED);
|
||||
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
||||
event = DMRDecodeEvent.builder(decodeEventType, timestamp)
|
||||
.channel(channel)
|
||||
.details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : ""))
|
||||
.identifiers(identifierCollection)
|
||||
.timeslot(channel.getTimeslot())
|
||||
.build();
|
||||
eventMap.put(frequency, event);
|
||||
}
|
||||
else if(event != null) //Update current event
|
||||
{
|
||||
Identifier from = getIdentifier(identifierCollection, Role.FROM);
|
||||
|
||||
if(from != null)
|
||||
{
|
||||
Identifier currentFrom = getIdentifier(event.getIdentifierCollection(), Role.FROM);
|
||||
if(currentFrom != null && !Objects.equals(from, currentFrom))
|
||||
{
|
||||
event.end(timestamp);
|
||||
|
||||
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
||||
event = DMRDecodeEvent.builder(decodeEventType, timestamp)
|
||||
.channel(channel)
|
||||
.details("CONTINUE - CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : ""))
|
||||
.identifiers(identifierCollection)
|
||||
.timeslot(channel.getTimeslot())
|
||||
.build();
|
||||
|
||||
eventMap.put(frequency, event);
|
||||
}
|
||||
}
|
||||
|
||||
//update the ending timestamp so that the duration value is correctly calculated
|
||||
event.update(timestamp);
|
||||
}
|
||||
|
||||
//If our channel doesn't have a frequency value, update the event to reflect this.
|
||||
if(channel.getDownlinkFrequency() == 0)
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(NO_FREQUENCY);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(NO_FREQUENCY))
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + NO_FREQUENCY);
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode())
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(DATA_CALL_IGNORED);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(DATA_CALL_IGNORED))
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + DATA_CALL_IGNORED);
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
return;
|
||||
}
|
||||
|
||||
Channel trafficChannel = mAvailableTrafficChannels.poll();
|
||||
|
||||
if(trafficChannel != null)
|
||||
{
|
||||
SourceConfigTuner sourceConfig = new SourceConfigTuner();
|
||||
sourceConfig.setFrequency(frequency);
|
||||
trafficChannel.setSourceConfiguration(sourceConfig);
|
||||
mAllocatedChannelFrequencyMap.put(frequency, trafficChannel);
|
||||
//Preload the channel grant event for the traffic channel to use/maintain
|
||||
if(trafficChannel.getDecodeConfiguration() instanceof DecodeConfigDMR config)
|
||||
{
|
||||
config.setChannelGrantEvent(event);
|
||||
}
|
||||
|
||||
getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel,
|
||||
identifierCollection, this));
|
||||
}
|
||||
else
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(MAX_TRAFFIC_CHANNELS_EXCEEDED))
|
||||
{
|
||||
event.setDetails(event.getDetails() + " - " + MAX_TRAFFIC_CHANNELS_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//This shouldn't happen, but we'll log if it ever does.
|
||||
mLog.error("Error while processing channel grant event - traffic channel lock is released", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -436,6 +511,8 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
{
|
||||
case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT:
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE:
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE:
|
||||
type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP;
|
||||
break;
|
||||
|
||||
@@ -540,7 +617,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
* @param currentIdentifiers to compare against the event
|
||||
* @return true if the event is stale.
|
||||
*/
|
||||
private boolean isStale(DecodeEvent event, long timestamp, IdentifierCollection currentIdentifiers)
|
||||
private boolean isStale(IDecodeEvent event, long timestamp, IdentifierCollection currentIdentifiers)
|
||||
{
|
||||
if(event == null)
|
||||
{
|
||||
@@ -609,23 +686,41 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
for(Long frequency: mAllocatedTrafficChannelFrequencyMap.keySet())
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(frequency));
|
||||
for(Long frequency: mAllocatedChannelFrequencyMap.keySet())
|
||||
{
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(frequency));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
mAvailableTrafficChannelQueue.clear();
|
||||
mLock.lock();
|
||||
|
||||
List<Channel> channels = new ArrayList<>(mAllocatedTrafficChannelFrequencyMap.values());
|
||||
|
||||
//Issue a disable request for each traffic channel
|
||||
for(Channel channel: channels)
|
||||
try
|
||||
{
|
||||
broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE));
|
||||
mAvailableTrafficChannels.clear();
|
||||
|
||||
List<Channel> channels = new ArrayList<>(mAllocatedChannelFrequencyMap.values());
|
||||
|
||||
//Issue a disable request for each traffic channel
|
||||
for(Channel channel: channels)
|
||||
{
|
||||
broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,115 +736,153 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
*/
|
||||
private void removeCallEvents(long frequency)
|
||||
{
|
||||
List<DMRChannel> channelsToRemove = new ArrayList<>();
|
||||
mLock.lock();
|
||||
|
||||
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||
try
|
||||
{
|
||||
if(entry.getKey().getDownlinkFrequency() == frequency)
|
||||
{
|
||||
channelsToRemove.add(entry.getKey());
|
||||
}
|
||||
mCallEventsTS1.remove(frequency);
|
||||
mCallEventsTS2.remove(frequency);
|
||||
}
|
||||
|
||||
for(DMRChannel channel: channelsToRemove)
|
||||
finally
|
||||
{
|
||||
mChannelGrantEventMap.remove(channel);
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCallEventDetails(long frequency, String detailFragment)
|
||||
/**
|
||||
* Updates any call events related to the frequency to indicate the channel start was rejected.
|
||||
* @param frequency for the channel.
|
||||
*/
|
||||
private void setChannelStartRejected(long frequency)
|
||||
{
|
||||
List<DMRChannel> channelsToUpdate = new ArrayList<>();
|
||||
mLock.lock();
|
||||
|
||||
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||
try
|
||||
{
|
||||
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
||||
IDecodeEvent event = mCallEventsTS1.get(frequency);
|
||||
|
||||
if(event instanceof DecodeEvent decodeEvent1)
|
||||
{
|
||||
channelsToUpdate.add(entry.getKey());
|
||||
if(decodeEvent1.getDetails() == null)
|
||||
{
|
||||
decodeEvent1.setDetails(CHANNEL_START_REJECTED);
|
||||
}
|
||||
else if(!decodeEvent1.getDetails().endsWith(CHANNEL_START_REJECTED))
|
||||
{
|
||||
decodeEvent1.setDetails(event.getDetails() + "-" + CHANNEL_START_REJECTED);
|
||||
}
|
||||
|
||||
broadcast(decodeEvent1);
|
||||
}
|
||||
|
||||
event = mCallEventsTS2.get(frequency);
|
||||
|
||||
if(event instanceof DecodeEvent decodeEvent2)
|
||||
{
|
||||
if(decodeEvent2.getDetails() == null)
|
||||
{
|
||||
decodeEvent2.setDetails(CHANNEL_START_REJECTED);
|
||||
}
|
||||
else if(!decodeEvent2.getDetails().endsWith(CHANNEL_START_REJECTED))
|
||||
{
|
||||
decodeEvent2.setDetails(decodeEvent2.getDetails() + "-" + CHANNEL_START_REJECTED);
|
||||
}
|
||||
|
||||
broadcast(decodeEvent2);
|
||||
}
|
||||
}
|
||||
|
||||
for(DMRChannel channel: channelsToUpdate)
|
||||
finally
|
||||
{
|
||||
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||
|
||||
if(event != null)
|
||||
{
|
||||
if(event.getDetails() == null)
|
||||
{
|
||||
event.setDetails(detailFragment);
|
||||
}
|
||||
else if(!event.getDetails().endsWith(detailFragment))
|
||||
{
|
||||
event.setDetails(event.getDetails() + detailFragment);
|
||||
}
|
||||
|
||||
broadcast(event);
|
||||
}
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Process channel events from the ChannelProcessingManager to account for owned child traffic channels.
|
||||
* Note: this method sees events for ALL channels and not just DMR channels managed by this instance.
|
||||
*
|
||||
* @param channelEvent to process
|
||||
*/
|
||||
@Override
|
||||
public synchronized void receive(ChannelEvent channelEvent)
|
||||
{
|
||||
Channel channel = channelEvent.getChannel();
|
||||
|
||||
if(channel.isTrafficChannel())
|
||||
//Only process the event if it's one of the traffic channels managed by this instance.
|
||||
if(mAllocatedTrafficChannels.contains(channel))
|
||||
{
|
||||
switch(channelEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_PROCESSING_STOP:
|
||||
long frequencyToRemove = 0;
|
||||
|
||||
for(Map.Entry<Long,Channel> entry: mAllocatedTrafficChannelFrequencyMap.entrySet())
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(channel == entry.getValue() && entry.getKey() != null)
|
||||
for(Map.Entry<Long,Channel> entry: mAllocatedChannelFrequencyMap.entrySet())
|
||||
{
|
||||
frequencyToRemove = entry.getKey();
|
||||
removeCallEvents(entry.getKey());
|
||||
break;
|
||||
if(channel == entry.getValue() && entry.getKey() != null)
|
||||
{
|
||||
frequencyToRemove = entry.getKey();
|
||||
removeCallEvents(entry.getKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(frequencyToRemove > 0)
|
||||
{
|
||||
mAllocatedChannelFrequencyMap.remove(frequencyToRemove);
|
||||
|
||||
//Unlock the frequency in the channel rotation monitor
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToRemove));
|
||||
}
|
||||
|
||||
//Add the traffic channel back to the queue to be reused
|
||||
if(!mAvailableTrafficChannels.contains(channel))
|
||||
{
|
||||
mAvailableTrafficChannels.add(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if(frequencyToRemove > 0)
|
||||
finally
|
||||
{
|
||||
mAllocatedTrafficChannelFrequencyMap.remove(frequencyToRemove);
|
||||
|
||||
//Unlock the frequency in the channel rotation monitor
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToRemove));
|
||||
}
|
||||
|
||||
//Add the traffic channel back to the queue to be reused
|
||||
if(!mAvailableTrafficChannelQueue.contains(channel))
|
||||
{
|
||||
mAvailableTrafficChannelQueue.add(channel);
|
||||
mLock.unlock();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_PROCESSING_START_REJECTED:
|
||||
long frequencyToUpdate = 0;
|
||||
|
||||
for(Map.Entry<Long,Channel> entry: mAllocatedTrafficChannelFrequencyMap.entrySet())
|
||||
mLock.lock();
|
||||
try
|
||||
{
|
||||
if(channel == entry.getValue() && entry.getKey() != null)
|
||||
long frequencyToUpdate = 0;
|
||||
|
||||
for(Map.Entry<Long,Channel> entry: mAllocatedChannelFrequencyMap.entrySet())
|
||||
{
|
||||
frequencyToUpdate = entry.getKey();
|
||||
updateCallEventDetails(entry.getKey(), CHANNEL_START_REJECTED);
|
||||
break;
|
||||
if(channel == entry.getValue() && entry.getKey() != null)
|
||||
{
|
||||
frequencyToUpdate = entry.getKey();
|
||||
setChannelStartRejected(entry.getKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(frequencyToUpdate > 0)
|
||||
{
|
||||
mAllocatedChannelFrequencyMap.remove(frequencyToUpdate);
|
||||
|
||||
//Unlock the frequency in the channel rotation monitor
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToUpdate));
|
||||
}
|
||||
|
||||
//Add the traffic channel back to the queue to be reused
|
||||
if(!mAvailableTrafficChannels.contains(channel))
|
||||
{
|
||||
mAvailableTrafficChannels.add(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if(frequencyToUpdate > 0)
|
||||
finally
|
||||
{
|
||||
mAllocatedTrafficChannelFrequencyMap.remove(frequencyToUpdate);
|
||||
|
||||
//Unlock the frequency in the channel rotation monitor
|
||||
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToUpdate));
|
||||
}
|
||||
|
||||
//Add the traffic channel back to the queue to be reused
|
||||
if(!mAvailableTrafficChannelQueue.contains(channel))
|
||||
{
|
||||
mAvailableTrafficChannelQueue.add(channel);
|
||||
mLock.unlock();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -41,6 +42,9 @@ public class DecodeConfigDMR extends DecodeConfiguration
|
||||
private boolean mUseCompressedTalkgroups = false;
|
||||
private List<TimeslotFrequency> mTimeslotMap = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
private DecodeEvent mChannelGrantEvent;
|
||||
|
||||
public DecodeConfigDMR()
|
||||
{
|
||||
}
|
||||
@@ -191,4 +195,32 @@ public class DecodeConfigDMR extends DecodeConfiguration
|
||||
{
|
||||
mTimeslotMap.add(timeslotFrequency);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
/**
|
||||
* Original channel grant event. This is used for trunking systems to pass the original channel grant event to
|
||||
* the decoder state on channel startup.
|
||||
*/
|
||||
public DecodeEvent getChannelGrantEvent()
|
||||
{
|
||||
return mChannelGrantEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the original channel grant event.
|
||||
* @param channelGrantEvent to set on the decoder state.
|
||||
*/
|
||||
public void setChannelGrantEvent(DecodeEvent channelGrantEvent)
|
||||
{
|
||||
mChannelGrantEvent = channelGrantEvent;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
/**
|
||||
* Indicates if this decode config has an original channel grant event to preload.
|
||||
*/
|
||||
public boolean hasChannelGrantEvent()
|
||||
{
|
||||
return mChannelGrantEvent != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages alternate CRC masks that may be employed in a system. Some vendors employ non-standard CRC checksum masks
|
||||
* or initial fill values. This utility tracks these non-standard values by observation count and time and accepts
|
||||
* these alternate mask values to automatically correct or validate messages when those messages employ these
|
||||
* alternate mask values. Alternate mask values must be observed at least 3x times in a 2-minute window against the
|
||||
* same message content, in order for the alternate mask value to be used. Tracked values that do not get another
|
||||
* observation count within a 2-minute window will be ejected from the cache and excluded as a valid CRC mask candidate.
|
||||
*
|
||||
* Notes: different alternate CRC mask values can be used independently in each timeslot. Further, I've seen multiple
|
||||
* CRC mask values being employed within the same timeslot. This indicates that systems can use multiple CRC mask
|
||||
* values against CSBK messages and some other function is involved in determining which CRC mask value to use when
|
||||
* checking the CRC of the transmitted CSBK ... fun and games!
|
||||
*/
|
||||
public class DmrCrcMaskManager
|
||||
{
|
||||
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
|
||||
private Map<Integer, MaskTracker> mCsbkTrackerMapTS1 = new TreeMap<>();
|
||||
private Map<Integer, MaskTracker> mCsbkTrackerMapTS2 = new TreeMap<>();
|
||||
private Set<MaskTracker> mSortedTrackersTS1 = new TreeSet<>();
|
||||
private Set<MaskTracker> mSortedTrackersTS2 = new TreeSet<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DmrCrcMaskManager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the current tracked CRC values and counts.
|
||||
*/
|
||||
public void log()
|
||||
{
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("DMR CRC Mask Manager - TS1 Current Tracked Values\n");
|
||||
for(MaskTracker tracker: mSortedTrackersTS1)
|
||||
{
|
||||
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
|
||||
sb.append(" Count:").append(tracker.getObservationCount());
|
||||
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
|
||||
}
|
||||
|
||||
sb.append("DMR CRC Mask Manager - TS2 Current Tracked Values\n");
|
||||
for(MaskTracker tracker: mSortedTrackersTS2)
|
||||
{
|
||||
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
|
||||
sb.append(" Count:").append(tracker.getObservationCount());
|
||||
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
|
||||
}
|
||||
|
||||
LOGGER.info(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
|
||||
* if so, attempts to correct the message.
|
||||
* @param csbk to re-check using the currently tracked mask value.
|
||||
*/
|
||||
public void check(CSBKMessage csbk)
|
||||
{
|
||||
if(!csbk.isValid())
|
||||
{
|
||||
int alternateMask = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
|
||||
|
||||
Map<Integer,MaskTracker> map;
|
||||
Set<MaskTracker> sortedSet;
|
||||
|
||||
if(csbk.getTimeslot() == 1)
|
||||
{
|
||||
map = mCsbkTrackerMapTS1;
|
||||
sortedSet = mSortedTrackersTS1;
|
||||
}
|
||||
else
|
||||
{
|
||||
map = mCsbkTrackerMapTS2;
|
||||
sortedSet = mSortedTrackersTS2;
|
||||
}
|
||||
|
||||
if(map.containsKey(alternateMask))
|
||||
{
|
||||
MaskTracker matchingTracker = map.get(alternateMask);
|
||||
matchingTracker.increment(csbk.getTimestamp());
|
||||
|
||||
if(matchingTracker.isValid())
|
||||
{
|
||||
csbk.checkCRC(matchingTracker.getTrackedValue());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Attempt to use other tracked mask values in order of observed count and also remove stale trackers
|
||||
boolean found = false;
|
||||
MaskTracker next;
|
||||
Iterator<MaskTracker> it = sortedSet.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
next = it.next();
|
||||
|
||||
//If the tracker is stale, remove it
|
||||
if(next.isStale(csbk.getTimestamp()))
|
||||
{
|
||||
it.remove();
|
||||
map.remove(next.getTrackedValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
//If we haven't found a match yet, attempt to correct the message using this tracked mask value
|
||||
if(!found)
|
||||
{
|
||||
csbk.checkCRC(next.getTrackedValue());
|
||||
|
||||
if(csbk.isValid())
|
||||
{
|
||||
found = true;
|
||||
next.increment(csbk.getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we didn't find a perfect match or even a close match (with 1 bit error), create a new tracked value.
|
||||
if(!found)
|
||||
{
|
||||
MaskTracker tracker = new MaskTracker(alternateMask, csbk.getTimestamp());
|
||||
map.put(alternateMask, tracker);
|
||||
sortedSet.add(tracker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate CRC mask value tracker. Tracks the number of observances of the mask value to detect when an alternate
|
||||
* CRC mask pattern is employed and can then be used to automatically correct CRC values for messages.
|
||||
*/
|
||||
public class MaskTracker implements Comparable<MaskTracker>
|
||||
{
|
||||
private static final long STALENESS_TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
|
||||
private static final int STALENESS_COUNT_THRESHOLD = 100;
|
||||
private int mTrackedValue;
|
||||
private int mObservationCount;
|
||||
private int mStalenessCount;
|
||||
private long mLastUpdated;
|
||||
|
||||
/**
|
||||
* Constructs an instance for the specified mask, sets the observation count to one and sets the last update to
|
||||
* the supplied timestamp.
|
||||
* @param residual value to track.
|
||||
* @param timestamp for the observation
|
||||
*/
|
||||
public MaskTracker(int residual, long timestamp)
|
||||
{
|
||||
mTrackedValue = residual;
|
||||
mObservationCount = 1;
|
||||
mLastUpdated = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of observations for this mask value.
|
||||
* @return count
|
||||
*/
|
||||
public int getObservationCount()
|
||||
{
|
||||
return mObservationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the residual value has been observed at least 3x times within the preceding 2 minute window,
|
||||
* indicating that it is (likely) valid.
|
||||
* @return true if the tracked mask value is valid.
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return mObservationCount >= 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this tracker is stale, relative to the supplied timestamp. Stale indicates that this tracked
|
||||
* value hasn't been observed in the preceding 2 minutes, or has been checked for staleness more than 100 times
|
||||
* without an updated observation.
|
||||
* @param timestamp to compare for staleness check.
|
||||
* @return true if this tracker is stale.
|
||||
*/
|
||||
public boolean isStale(long timestamp)
|
||||
{
|
||||
mStalenessCount++;
|
||||
return mStalenessCount > STALENESS_COUNT_THRESHOLD ||
|
||||
Math.abs(timestamp - mLastUpdated) > STALENESS_TIME_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp when this value was last observed.
|
||||
*/
|
||||
public long getLastUpdated()
|
||||
{
|
||||
return mLastUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracked alternate mask value.
|
||||
* @return tracked mask value
|
||||
*/
|
||||
public int getTrackedValue()
|
||||
{
|
||||
return mTrackedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the observation count and use the supplied timestamp as the last updated timestamp.
|
||||
* @param timestamp for the observed message.
|
||||
*/
|
||||
public void increment(long timestamp)
|
||||
{
|
||||
mStalenessCount = 0;
|
||||
mObservationCount++;
|
||||
//Detect integer rollover and apply max integer value.
|
||||
if(mObservationCount < 0)
|
||||
{
|
||||
mObservationCount = Integer.MAX_VALUE;
|
||||
}
|
||||
mLastUpdated = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom (reversed) sort order by largest count and then by latest updated timestamp.
|
||||
* @param o the object to be compared.
|
||||
* @return comparison value.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(MaskTracker o)
|
||||
{
|
||||
//Multiply by -1 to apply a reverse ordering
|
||||
int comparison = Integer.compare(getObservationCount(), o.getObservationCount()) * -1;
|
||||
|
||||
if(comparison == 0)
|
||||
{
|
||||
comparison = Long.compare(getLastUpdated(), o.getLastUpdated()) * -1;
|
||||
}
|
||||
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,4 +74,12 @@ public class DMRAbsoluteChannel extends DMRChannel
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DMRChannel getSisterTimeslot()
|
||||
{
|
||||
DMRAbsoluteChannel other = new DMRAbsoluteChannel(getChannelNumber(), getTimeslot() == 1 ? 2 : 1,
|
||||
getDownlinkFrequency(), getUplinkFrequency());
|
||||
return other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel of the same type, but for the alternate timeslot.
|
||||
* @return sister timeslot channel
|
||||
*/
|
||||
public abstract DMRChannel getSisterTimeslot();
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
@@ -160,6 +166,15 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
||||
mTimeslotFrequency = timeslotFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* TimeslotFrequency for this channel.
|
||||
* @return timeslot frequency instance, or null.
|
||||
*/
|
||||
public TimeslotFrequency getTimeslotFrequency()
|
||||
{
|
||||
return mTimeslotFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
|
||||
@@ -69,4 +69,25 @@ public class DMRLsn extends DMRChannel
|
||||
return ((getChannelNumber() - 1) * 2) + getTimeslot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel of the same type, but for the alternate timeslot.
|
||||
* @return sister timeslot channel
|
||||
*/
|
||||
public DMRChannel getSisterTimeslot()
|
||||
{
|
||||
int lsn;
|
||||
|
||||
if(getTimeslot() == 1)
|
||||
{
|
||||
lsn = getLsn() + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
lsn = getLsn() - 1;
|
||||
}
|
||||
|
||||
DMRLsn other = new DMRLsn(lsn);
|
||||
other.setTimeslotFrequency(getTimeslotFrequency());
|
||||
return other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,12 @@ public class DMRTier3Channel extends DMRChannel
|
||||
sb.append(" CHANID:").append(getChannelId());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DMRChannel getSisterTimeslot()
|
||||
{
|
||||
DMRTier3Channel other = new DMRTier3Channel(getChannelNumber(), getTimeslot() == 1 ? 2 : 1);
|
||||
other.setTimeslotFrequency(getTimeslotFrequency());
|
||||
return other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.dmr.event;
|
||||
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||
|
||||
/**
|
||||
* DMR Channel Grant Event.
|
||||
*
|
||||
* This event class is send traffic channel grant details to the traffic channel manager to allocate a traffic channel.
|
||||
*/
|
||||
public class DMRChannelGrantEvent extends DMRDecodeEvent
|
||||
{
|
||||
private boolean mEncrypted;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param decodeEventType for the event
|
||||
* @param timestamp for the event
|
||||
*/
|
||||
public DMRChannelGrantEvent(DecodeEventType decodeEventType, long timestamp)
|
||||
{
|
||||
super(decodeEventType, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the channel grant was for an encrypted call
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return mEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encrypte state for the call
|
||||
* @param encrypted
|
||||
*/
|
||||
public void setEncrypted(boolean encrypted)
|
||||
{
|
||||
mEncrypted = encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new decode event builder with the specified start timestamp.
|
||||
* @param timeStart for the event
|
||||
* @return builder
|
||||
*/
|
||||
public static DMRChannelGrantDecodeEventBuilder channelGrantBuilder(DecodeEventType decodeEventType, long timeStart)
|
||||
{
|
||||
return new DMRChannelGrantDecodeEventBuilder(decodeEventType, timeStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern for constructing decode events.
|
||||
*/
|
||||
public static class DMRChannelGrantDecodeEventBuilder
|
||||
{
|
||||
protected long mTimeStart;
|
||||
protected long mDuration;
|
||||
protected DecodeEventType mDecodeEventType;
|
||||
protected IdentifierCollection mIdentifierCollection;
|
||||
protected DMRChannel mChannel;
|
||||
protected String mDetails;
|
||||
protected boolean mEncrypted;
|
||||
|
||||
/**
|
||||
* Constructs a builder instance with the specified start time in milliseconds
|
||||
*
|
||||
* @param timeStart
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder(DecodeEventType decodeEventType, long timeStart)
|
||||
{
|
||||
mDecodeEventType = decodeEventType;
|
||||
mTimeStart = timeStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encrypted state for the channel/timeslot
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder encrypted(boolean encrypted)
|
||||
{
|
||||
mEncrypted = encrypted;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration value
|
||||
* @param duration in milliseconds
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder duration(long duration)
|
||||
{
|
||||
mDuration = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration value using the end - start timestamps
|
||||
* @param timestamp for end of event
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder end(long timestamp)
|
||||
{
|
||||
mDuration = timestamp - mTimeStart;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel descriptor for this event
|
||||
* @param channel
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder channel(DMRChannel channel)
|
||||
{
|
||||
mChannel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identifier collection.
|
||||
* @param identifierCollection containing optional identifiers like TO, FROM, frequency and
|
||||
* alias list configuration name.
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder identifiers(IdentifierCollection identifierCollection)
|
||||
{
|
||||
mIdentifierCollection = identifierCollection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the details for the event
|
||||
* @param details
|
||||
*/
|
||||
public DMRChannelGrantDecodeEventBuilder details(String details)
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the decode event
|
||||
*/
|
||||
public DMRChannelGrantEvent build()
|
||||
{
|
||||
DMRChannelGrantEvent decodeEvent = new DMRChannelGrantEvent(mDecodeEventType, mTimeStart);
|
||||
decodeEvent.setChannelDescriptor(mChannel);
|
||||
decodeEvent.setTimeslot(mChannel.getTimeslot());
|
||||
decodeEvent.setDetails(mDetails);
|
||||
decodeEvent.setDuration(mDuration);
|
||||
decodeEvent.setIdentifierCollection(mIdentifierCollection);
|
||||
return decodeEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.identifier;
|
||||
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.location.LocationIdentifier;
|
||||
import io.github.dsheirer.identifier.location.Point;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* P25 location identifier for user radio reported GPS positions.
|
||||
*/
|
||||
public class P25Location extends LocationIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param value location
|
||||
* @param role to or from
|
||||
*/
|
||||
public P25Location(Point value, Role role)
|
||||
{
|
||||
super(value, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a new DMR location
|
||||
* @param latitude of the position
|
||||
* @param longitude of the position
|
||||
* @return constructed DMR location identifier
|
||||
*/
|
||||
public static P25Location createFrom(double latitude, double longitude)
|
||||
{
|
||||
Point point = new Point(latitude, longitude);
|
||||
return new P25Location(point, Role.FROM);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
||||
|
||||
@@ -72,6 +71,17 @@ public abstract class CSBKMessage extends DataMessage
|
||||
setValid(correctedBitCount < 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks CRC using an alternate mask value and sets the message valid flag according to the results.
|
||||
*/
|
||||
public void checkCRC(int mask)
|
||||
{
|
||||
int correctedBitCount = CRCDMR.correctCCITT80(getMessage(), 0, 80, mask);
|
||||
|
||||
//Set message valid flag according to the corrected bit count for the CRC protected message
|
||||
setValid(correctedBitCount < 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks crc for multi-block CSBK where the continuation block is an absolute channel parameters block
|
||||
*
|
||||
|
||||
@@ -29,11 +29,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAdjac
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko44;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko47;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraSmsAvailableNotification;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxOpenModeVoiceChannelUpdate;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAdvantageModeVoiceChannelUpdate;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusCSBKO_60;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusDataRevertWindowAnnouncement;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusDataRevertWindowGrant;
|
||||
@@ -250,8 +252,8 @@ public class CSBKMessageFactory
|
||||
case HYTERA_08_CSBKO_44:
|
||||
csbk = new HyteraCsbko44(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case HYTERA_08_CSBKO_47:
|
||||
csbk = new HyteraCsbko47(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||
csbk = new HyteraTrafficChannelTalkerStatus(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case HYTERA_68_ACKNOWLEDGE:
|
||||
csbk = new Hytera68Acknowledge(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
@@ -277,7 +279,12 @@ public class CSBKMessageFactory
|
||||
case MOTOROLA_CAPPLUS_DATA_WINDOW_GRANT:
|
||||
csbk = new CapacityPlusDataRevertWindowGrant(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE:
|
||||
csbk = new CapacityMaxOpenModeVoiceChannelUpdate(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE:
|
||||
csbk = new CapacityMaxAdvantageModeVoiceChannelUpdate(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
case MOTOROLA_CONPLUS_CSBKO_10:
|
||||
csbk = new ConnectPlusOTAAnnouncement(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
break;
|
||||
|
||||
@@ -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
|
||||
@@ -77,7 +77,9 @@ public enum Opcode
|
||||
MOTOROLA_CONPLUS_DATA_WINDOW_ANNOUNCEMENT(Vendor.MOTOROLA_CONNECT_PLUS, 28, "ENHANCED DATA REVERT WINDOW ANNOUNCEMENT"),
|
||||
MOTOROLA_CONPLUS_DATA_WINDOW_GRANT(Vendor.MOTOROLA_CONNECT_PLUS, 29, "ENHANCED DATA REVERT WINDOW GRANT"),
|
||||
|
||||
MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "ALOHA"),
|
||||
MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "CAP MAX ALOHA"),
|
||||
MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE(Vendor.MOTOROLA_CAPACITY_PLUS, 33, "CAP MAX CHAN UPD OPEN MODE"),
|
||||
MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE(Vendor.MOTOROLA_CAPACITY_PLUS, 34, "CAP MAX CHAN UPD ADV MODE"),
|
||||
|
||||
MOTOROLA_CAPPLUS_CALL_ALERT(Vendor.MOTOROLA_CAPACITY_PLUS, 31, "CALL ALERT"),
|
||||
MOTOROLA_CAPPLUS_CALL_ALERT_ACK(Vendor.MOTOROLA_CAPACITY_PLUS, 32, "CALL ALERT ACK"),
|
||||
@@ -93,7 +95,7 @@ public enum Opcode
|
||||
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
|
||||
//and 47 was only transmitted 3x times in succession in the middle of the terminators and CSBKO 44 messages.
|
||||
HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"),
|
||||
HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
|
||||
HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
|
||||
|
||||
HYTERA_68_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA 68 XPT SITE STATE"),
|
||||
HYTERA_68_ALOHA(Vendor.HYTERA_68, 25, "HYTERA 68 ALOHA"),
|
||||
@@ -201,7 +203,7 @@ public enum Opcode
|
||||
* Hytera opcodes
|
||||
*/
|
||||
public static final EnumSet<Opcode> HYTERA = EnumSet.of(HYTERA_08_ACKNOWLEDGE, HYTERA_08_ANNOUNCEMENT,
|
||||
HYTERA_08_CSBKO_44, HYTERA_08_CSBKO_47, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
||||
HYTERA_08_CSBKO_44, HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
||||
HYTERA_68_ACKNOWLEDGE, HYTERA_68_ANNOUNCEMENT, HYTERA_68_XPT_PREAMBLE, HYTERA_68_CSBKO_62);
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,9 @@ import java.util.List;
|
||||
* Analysis: ... this was transmitted on the Tier III traffic channel during call termination. This CSBK was
|
||||
* interleaved with every other Terminator message, starting after the first 2x terminator messages. This continued
|
||||
* through the final teardown which was 3x Clear messages.
|
||||
*
|
||||
* On a second example, there were 8 distinct configurations of this message that were repeated in-order, multiple times
|
||||
* during the traffic channel shutdown ... a total of 51x of these messages were transmitted.
|
||||
*/
|
||||
public class HyteraCsbko44 extends CSBKMessage
|
||||
{
|
||||
|
||||
@@ -33,13 +33,14 @@ import java.util.List;
|
||||
/**
|
||||
* Hytera DMR Tier III - CSBKO 47 -
|
||||
*
|
||||
* Analysis: ... this was transmitted on the traffic channel while the mobile was at the end of a call, in the terminator
|
||||
* sequence. Since this comes from the SMS gateway 16776906, I wonder if this is a traffic channel message waiting notification?
|
||||
* Analysis: this message is transmitted three times at the beginning or end of a voice transmission and appears to
|
||||
* indicate that the traffic channel continues active and signals the start/end of an active speaker.
|
||||
*/
|
||||
public class HyteraCsbko47 extends CSBKMessage
|
||||
public class HyteraTrafficChannelTalkerStatus extends CSBKMessage
|
||||
{
|
||||
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static final int TALKER_INACTIVE = 30;
|
||||
private static final int[] DESTINATION_RADIO = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] SOURCE_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
|
||||
@@ -59,7 +60,7 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
* @param timestamp
|
||||
* @param timeslot
|
||||
*/
|
||||
public HyteraCsbko47(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
public HyteraTrafficChannelTalkerStatus(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
@@ -79,17 +80,39 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
sb.append(" HYTERA CSBKO=47 ######### UNKNOWN");
|
||||
sb.append(" HYTERA TRAFFIC CHANNEL");
|
||||
if(isChannelInactive())
|
||||
{
|
||||
sb.append(" NOW AVAILABLE - PREVIOUS TALKER:").append(getDestinationRadio());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" IN USE - CURRENT TALKER:").append(getDestinationRadio());
|
||||
}
|
||||
sb.append(" FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" UNK1:").append(getUnknown1());
|
||||
sb.append(" UNK2:").append(getUnknown2());
|
||||
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the channel is inactive and available for use.
|
||||
*/
|
||||
public boolean isChannelInactive()
|
||||
{
|
||||
return getMessage().get(TALKER_INACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the channel is active and in-use by the radio identifiers
|
||||
*/
|
||||
public boolean isChannelActive()
|
||||
{
|
||||
return !isChannelInactive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Addressed (ie TO) radio identifier
|
||||
* @return radio identifier
|
||||
@@ -108,7 +131,6 @@ public class HyteraCsbko47 extends CSBKMessage
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN_1, 2);
|
||||
}
|
||||
|
||||
public String getUnknown2()
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN_2, 2);
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Motorola Capacity Max Advantage Mode - Voice Channel Update (Opcode 34 / 0x22)
|
||||
* Note: it appears that Capacity Max Talkgroups in advantage mode are only 10-bits long in range of 0 - 1023.
|
||||
*/
|
||||
public class CapacityMaxAdvantageModeVoiceChannelUpdate extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
{
|
||||
private static final int[] CHANNEL_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27};
|
||||
private static final int[] TALKGROUP_CH1_TS1 = new int[]{28, 29, 30, 31, 32, 33, 34, 35, 36, 37};
|
||||
private static final int[] TALKGROUP_CH1_TS2 = new int[]{38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
private static final int[] CHANNEL_2 = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59};
|
||||
private static final int[] TALKGROUP_CH2_TS1 = new int[]{60, 61, 62, 63, 64, 65, 66, 67, 68, 69};
|
||||
private static final int[] TALKGROUP_CH2_TS2 = new int[]{70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
|
||||
private TalkgroupIdentifier mTalkgroupCH1TS1;
|
||||
private TalkgroupIdentifier mTalkgroupCH1TS2;
|
||||
private TalkgroupIdentifier mTalkgroupCH2TS1;
|
||||
private TalkgroupIdentifier mTalkgroupCH2TS2;
|
||||
private DMRTier3Channel mChannel1TS1;
|
||||
private DMRTier3Channel mChannel1TS2;
|
||||
private DMRTier3Channel mChannel2TS1;
|
||||
private DMRTier3Channel mChannel2TS2;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param syncPattern for the CSBK
|
||||
* @param message bits
|
||||
* @param cach for the DMR burst
|
||||
* @param slotType for this message
|
||||
* @param timestamp
|
||||
* @param timeslot
|
||||
*/
|
||||
public CapacityMaxAdvantageModeVoiceChannelUpdate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("[CRC-ERROR] ");
|
||||
}
|
||||
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
|
||||
sb.append(" CSBK CAP-MAX MODE:ADVANTAGE CHANNEL UPDATE");
|
||||
|
||||
if(hasChannel1Timeslot1())
|
||||
{
|
||||
sb.append(" TG1:").append(getTalkgroupCH1TS1());
|
||||
sb.append(" ON").append(getChannel1TS1());
|
||||
}
|
||||
if(hasChannel1Timeslot2())
|
||||
{
|
||||
sb.append(" TG2:").append(getTalkgroupCH1TS2());
|
||||
sb.append(" ON").append(getChannel1TS2());
|
||||
}
|
||||
|
||||
if(hasChannel2Timeslot1())
|
||||
{
|
||||
sb.append(" TG3:").append(getTalkgroupCH1TS1());
|
||||
sb.append(" ON").append(getChannel2TS1());
|
||||
}
|
||||
if(hasChannel2Timeslot2())
|
||||
{
|
||||
sb.append(" TG4:").append(getTalkgroupCH1TS2());
|
||||
sb.append(" ON").append(getChannel2TS2());
|
||||
}
|
||||
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains activity on channel 1 timeslot 1.
|
||||
*/
|
||||
public boolean hasChannel1Timeslot1()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_CH1_TS1) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains activity on channel 1 timeslot 2.
|
||||
*/
|
||||
public boolean hasChannel1Timeslot2()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_CH1_TS2) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains activity on channel 2 timeslot 1.
|
||||
*/
|
||||
public boolean hasChannel2Timeslot1()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_CH2_TS1) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains activity on channel 2 timeslot 2.
|
||||
*/
|
||||
public boolean hasChannel2Timeslot2()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_CH2_TS2) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on channel 1 timeslot 1.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupCH1TS1()
|
||||
{
|
||||
if(mTalkgroupCH1TS1 == null)
|
||||
{
|
||||
mTalkgroupCH1TS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH1_TS1));
|
||||
}
|
||||
|
||||
return mTalkgroupCH1TS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on channel 1 timeslot 2.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupCH1TS2()
|
||||
{
|
||||
if(mTalkgroupCH1TS2 == null)
|
||||
{
|
||||
mTalkgroupCH1TS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH1_TS2));
|
||||
}
|
||||
|
||||
return mTalkgroupCH1TS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on channel 2 timeslot 1.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupCH2TS1()
|
||||
{
|
||||
if(mTalkgroupCH2TS1 == null)
|
||||
{
|
||||
mTalkgroupCH2TS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH2_TS1));
|
||||
}
|
||||
|
||||
return mTalkgroupCH2TS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on channel 2 timeslot 2.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupCH2TS2()
|
||||
{
|
||||
if(mTalkgroupCH2TS2 == null)
|
||||
{
|
||||
mTalkgroupCH2TS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH2_TS2));
|
||||
}
|
||||
|
||||
return mTalkgroupCH2TS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel 1 Timeslot 1.
|
||||
*/
|
||||
public DMRTier3Channel getChannel1TS1()
|
||||
{
|
||||
if(mChannel1TS1 == null)
|
||||
{
|
||||
mChannel1TS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_1), 1);
|
||||
}
|
||||
|
||||
return mChannel1TS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel 1 Timeslot 2.
|
||||
*/
|
||||
public DMRTier3Channel getChannel1TS2()
|
||||
{
|
||||
if(mChannel1TS2 == null)
|
||||
{
|
||||
mChannel1TS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_1), 2);
|
||||
}
|
||||
|
||||
return mChannel1TS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel 2 Timeslot 1.
|
||||
*/
|
||||
public DMRTier3Channel getChannel2TS1()
|
||||
{
|
||||
if(mChannel2TS1 == null)
|
||||
{
|
||||
mChannel2TS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_2), 1);
|
||||
}
|
||||
|
||||
return mChannel2TS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel 2 Timeslot 2.
|
||||
*/
|
||||
public DMRTier3Channel getChannel2TS2()
|
||||
{
|
||||
if(mChannel2TS2 == null)
|
||||
{
|
||||
mChannel2TS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_2), 2);
|
||||
}
|
||||
|
||||
return mChannel2TS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a timeslot frequency map for the DMR channel
|
||||
*
|
||||
* @param timeslotFrequencies that match the logical timeslots
|
||||
*/
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
if(hasChannel1Timeslot1())
|
||||
{
|
||||
getChannel1TS1().apply(timeslotFrequencies);
|
||||
}
|
||||
if(hasChannel1Timeslot2())
|
||||
{
|
||||
getChannel1TS2().apply(timeslotFrequencies);
|
||||
}
|
||||
if(hasChannel2Timeslot1())
|
||||
{
|
||||
getChannel2TS1().apply(timeslotFrequencies);
|
||||
}
|
||||
if(hasChannel2Timeslot2())
|
||||
{
|
||||
getChannel2TS2().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
//Since both timeslots share the same LCN, we use just TS1's channel for both channel 1 and channel 2
|
||||
if((hasChannel1Timeslot1() || hasChannel1Timeslot2()) && (hasChannel2Timeslot1() || hasChannel2Timeslot2()))
|
||||
{
|
||||
return new int[]{getChannel1TS1().getChannelId(), getChannel2TS1().getChannelId()};
|
||||
}
|
||||
else if((hasChannel1Timeslot1() || hasChannel1Timeslot2()))
|
||||
{
|
||||
return new int[]{getChannel1TS1().getChannelId()};
|
||||
}
|
||||
else if((hasChannel2Timeslot1() || hasChannel2Timeslot2()))
|
||||
{
|
||||
return new int[]{getChannel2TS1().getChannelId()};
|
||||
}
|
||||
|
||||
return getChannel1TS1().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifiers == null)
|
||||
{
|
||||
mIdentifiers = new ArrayList<>();
|
||||
|
||||
if(hasChannel1Timeslot1())
|
||||
{
|
||||
mIdentifiers.add(getTalkgroupCH1TS1());
|
||||
mIdentifiers.add(getChannel1TS1());
|
||||
}
|
||||
|
||||
if(hasChannel1Timeslot2())
|
||||
{
|
||||
mIdentifiers.add(getTalkgroupCH1TS2());
|
||||
mIdentifiers.add(getChannel1TS2());
|
||||
}
|
||||
|
||||
if(hasChannel2Timeslot1())
|
||||
{
|
||||
mIdentifiers.add(getTalkgroupCH2TS1());
|
||||
mIdentifiers.add(getChannel2TS1());
|
||||
}
|
||||
|
||||
if(hasChannel2Timeslot2())
|
||||
{
|
||||
mIdentifiers.add(getTalkgroupCH2TS2());
|
||||
mIdentifiers.add(getChannel2TS2());
|
||||
}
|
||||
}
|
||||
|
||||
return mIdentifiers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Motorola Capacity Max Open System Mode - Voice Channel Update (Opcode 33 / 0x21)
|
||||
*
|
||||
* Opcode 33 and 34 are very similar. However, Opcode 33 appears to support full 24-bit group identifiers whereas
|
||||
* Opcode 34 only supports 10-bit group identifiers.
|
||||
* <p>
|
||||
* Likely incomplete implementation.
|
||||
*/
|
||||
public class CapacityMaxOpenModeVoiceChannelUpdate extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
{
|
||||
private static final int[] CHANNEL_NUMBER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27};
|
||||
private static final int[] TALKGROUP_TS1 = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] TALKGROUP_TS2 = new int[]{56, 57, 58, 59, 60, 61, 63, 64, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
|
||||
private TalkgroupIdentifier mTalkgroupTS1;
|
||||
private TalkgroupIdentifier mTalkgroupTS2;
|
||||
private DMRTier3Channel mChannelTS1;
|
||||
private DMRTier3Channel mChannelTS2;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param syncPattern for the CSBK
|
||||
* @param message bits
|
||||
* @param cach for the DMR burst
|
||||
* @param slotType for this message
|
||||
* @param timestamp
|
||||
* @param timeslot
|
||||
*/
|
||||
public CapacityMaxOpenModeVoiceChannelUpdate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("[CRC-ERROR] ");
|
||||
}
|
||||
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
|
||||
sb.append(" CSBK CAP-MAX MODE:OPEN-SYSTEM CHANNEL UPDATE");
|
||||
|
||||
if(hasTimeslot1())
|
||||
{
|
||||
sb.append(" TG1:").append(getTalkgroupTS1());
|
||||
sb.append(" ON").append(getChannelTS1());
|
||||
}
|
||||
if(hasTimeslot2())
|
||||
{
|
||||
sb.append(" TG2:").append(getTalkgroupTS2());
|
||||
sb.append(" ON").append(getChannelTS2());
|
||||
}
|
||||
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains channel update for timeslot 1.
|
||||
*/
|
||||
public boolean hasTimeslot1()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_TS1) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message contains channel update for timeslot 2.
|
||||
*/
|
||||
public boolean hasTimeslot2()
|
||||
{
|
||||
return getMessage().getInt(TALKGROUP_TS2) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on the channel for timeslot 1.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupTS1()
|
||||
{
|
||||
if(mTalkgroupTS1 == null)
|
||||
{
|
||||
mTalkgroupTS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_TS1));
|
||||
}
|
||||
|
||||
return mTalkgroupTS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup that is active on the channel for timeslot 2.
|
||||
*/
|
||||
public TalkgroupIdentifier getTalkgroupTS2()
|
||||
{
|
||||
if(mTalkgroupTS2 == null)
|
||||
{
|
||||
mTalkgroupTS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_TS2));
|
||||
}
|
||||
|
||||
return mTalkgroupTS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel for timeslot 1
|
||||
*/
|
||||
public DMRTier3Channel getChannelTS1()
|
||||
{
|
||||
if(mChannelTS1 == null)
|
||||
{
|
||||
mChannelTS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_NUMBER), 1);
|
||||
}
|
||||
|
||||
return mChannelTS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel for timeslot 2
|
||||
*/
|
||||
public DMRTier3Channel getChannelTS2()
|
||||
{
|
||||
if(mChannelTS2 == null)
|
||||
{
|
||||
mChannelTS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_NUMBER), 2);
|
||||
}
|
||||
|
||||
return mChannelTS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a timeslot frequency map for the DMR channel
|
||||
*
|
||||
* @param timeslotFrequencies that match the logical timeslots
|
||||
*/
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
if(hasTimeslot1())
|
||||
{
|
||||
getChannelTS1().apply(timeslotFrequencies);
|
||||
}
|
||||
if(hasTimeslot2())
|
||||
{
|
||||
getChannelTS2().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
boolean ts1 = hasTimeslot1();
|
||||
boolean ts2 = hasTimeslot2();
|
||||
|
||||
if(ts1 && ts2)
|
||||
{
|
||||
return new int[]{getChannelTS1().getChannelNumber(), getChannelTS2().getChannelNumber()};
|
||||
}
|
||||
else if(ts1)
|
||||
{
|
||||
return new int[]{getChannelTS1().getChannelNumber()};
|
||||
}
|
||||
else if(ts2)
|
||||
{
|
||||
return new int[]{getChannelTS2().getChannelNumber()};
|
||||
}
|
||||
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifiers == null)
|
||||
{
|
||||
mIdentifiers = new ArrayList<>();
|
||||
|
||||
if(hasTimeslot1())
|
||||
{
|
||||
mIdentifiers.add(getChannelTS1());
|
||||
mIdentifiers.add(getTalkgroupTS1());
|
||||
}
|
||||
if(hasTimeslot2())
|
||||
{
|
||||
mIdentifiers.add(getChannelTS2());
|
||||
mIdentifiers.add(getTalkgroupTS2());
|
||||
}
|
||||
}
|
||||
|
||||
return mIdentifiers;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -21,15 +21,25 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Capacity+ Site Status CSBKO=62 Message
|
||||
@@ -43,6 +53,8 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
private static final int[] LSN_VOICE_BITMAP = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static final int LSN_1_8_BITMAP_START = 24;
|
||||
|
||||
private Map<Integer,IntegerIdentifier> mActiveIdentifierMap;
|
||||
private Map<Integer,DMRLsn> mActiveLsnMap;
|
||||
private DmrRestLsn mRestChannel;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
@@ -96,172 +108,202 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message has voice talkgroup activity.
|
||||
*
|
||||
* @return true if voice activity.
|
||||
*/
|
||||
private boolean hasVoiceTalkgroups()
|
||||
{
|
||||
return getSegmentIndicator().isFirst() && getMessage().getInt(LSN_VOICE_BITMAP) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the call activity from the first or first/last fragment.
|
||||
* @return description of call activity.
|
||||
*/
|
||||
private String getActivityFragments()
|
||||
{
|
||||
if(getMessage().get(48) && getMessage().get(56))
|
||||
{
|
||||
int a = 0; //debug
|
||||
}
|
||||
int[] talkgroups = new int[17];
|
||||
int[] radios = new int[17];
|
||||
boolean hasIdentifier = false;
|
||||
|
||||
int pointer = LSN_1_8_BITMAP_START;
|
||||
|
||||
//Process voice LSNs 1-8
|
||||
if(getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
int lowLsnBitmap = pointer;
|
||||
pointer += 8;
|
||||
|
||||
for(int x = lowLsnBitmap; x < (lowLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - lowLsnBitmap + 1;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
hasIdentifier = true;
|
||||
|
||||
if(pointer <= 72)
|
||||
{
|
||||
talkgroups[lsn] = getMessage().getInt(BYTE, pointer);
|
||||
pointer += 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
talkgroups[lsn] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer += 8;
|
||||
}
|
||||
|
||||
//Process voice LSNs 9 - 16
|
||||
if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
hasIdentifier = true;
|
||||
int highLsnBitmap = pointer;
|
||||
|
||||
pointer += 8;
|
||||
|
||||
for(int x = highLsnBitmap; x < (highLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - highLsnBitmap + 1 + 8;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
if(pointer <= 72)
|
||||
{
|
||||
talkgroups[lsn] = getMessage().getInt(BYTE, pointer);
|
||||
pointer += 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
talkgroups[lsn] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer += 8;
|
||||
}
|
||||
|
||||
//Process Radio IDs - first bit in radio options byte is set to indicate more activity
|
||||
if(pointer <= 72 && getMessage().get(pointer))
|
||||
{
|
||||
hasIdentifier = true;
|
||||
pointer += 8;
|
||||
|
||||
//If we have the data revert channel bitmap ...
|
||||
if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
int lowDataLsnBitmap = pointer;
|
||||
|
||||
pointer += 8;
|
||||
|
||||
for(int x = lowDataLsnBitmap; x < (lowDataLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - lowDataLsnBitmap + 1;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
if(pointer <= 64)
|
||||
{
|
||||
radios[lsn] = getMessage().getInt(TWO_BYTES, pointer);
|
||||
pointer += 16;
|
||||
}
|
||||
else
|
||||
{
|
||||
radios[lsn] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!hasIdentifier)
|
||||
if(getActiveIdentifierMap().isEmpty())
|
||||
{
|
||||
sb.append("IDLE LSN: 1-16");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("ACTIVE LSN");
|
||||
|
||||
for(int x = 1; x < 17; x++)
|
||||
{
|
||||
if(talkgroups[x] != 0 || radios[x] != 0)
|
||||
{
|
||||
sb.append(" ").append(x);
|
||||
sb.append(" ").append(x);
|
||||
|
||||
if(talkgroups[x] == 0)
|
||||
if(mActiveIdentifierMap.containsKey(x))
|
||||
{
|
||||
Identifier id = mActiveIdentifierMap.get(x);
|
||||
|
||||
if(id instanceof TalkgroupIdentifier talkgroup)
|
||||
{
|
||||
if(radios[x] < 0)
|
||||
if(talkgroup.getValue() > 0)
|
||||
{
|
||||
sb.append(":(R)A"); //Radio active on channel but identifier is in a continuation message
|
||||
}
|
||||
else if(radios[x] > 0)
|
||||
{
|
||||
sb.append(":(R)").append(radios[x]); //Active radio ID for channel
|
||||
sb.append(":(T)").append(talkgroup.getValue()); //Active talkgroup ID for channel.
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(":"); //No radio or talkgroup active on channel
|
||||
sb.append(":(T)A"); //Talkgroup active on channel but identifier is in a continuation message
|
||||
}
|
||||
}
|
||||
else if(talkgroups[x] < 0)
|
||||
else if(id instanceof RadioIdentifier radio)
|
||||
{
|
||||
sb.append(":(T)A"); //Talkgroup active on channel but identifier is in a continuation message
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(":(T)").append(talkgroups[x]); //Active talkgroup ID for channel.
|
||||
if(radio.getValue() > 0)
|
||||
{
|
||||
sb.append(":(R)").append(radio.getValue()); //Active radio ID for channel
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(":(R)A"); //Radio active on channel but identifier is in a continuation message
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(":"); //No radio or talkgroup active on channel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message has voice talkgroup activity.
|
||||
*
|
||||
* @return true if voice activity.
|
||||
*/
|
||||
public boolean hasVoiceTalkgroups()
|
||||
{
|
||||
return getSegmentIndicator().isFirst() && getMessage().getInt(LSN_VOICE_BITMAP) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of active LSNs.
|
||||
* @return map of active LSNs
|
||||
*/
|
||||
public Map<Integer,DMRLsn> getActiveLsnMap()
|
||||
{
|
||||
if(mActiveLsnMap == null)
|
||||
{
|
||||
getActiveIdentifierMap();
|
||||
}
|
||||
|
||||
return mActiveLsnMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of active voice talkgroups and DMR logical slots where they are active.
|
||||
*/
|
||||
public Map<Integer,IntegerIdentifier> getActiveIdentifierMap()
|
||||
{
|
||||
if(mActiveIdentifierMap == null)
|
||||
{
|
||||
mActiveIdentifierMap = new HashMap<>();
|
||||
mActiveLsnMap = new HashMap<>();
|
||||
|
||||
int pointer = LSN_1_8_BITMAP_START;
|
||||
|
||||
//Process voice LSNs 1-8
|
||||
if(getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
int lowLsnBitmap = pointer;
|
||||
pointer += 8;
|
||||
|
||||
for(int x = lowLsnBitmap; x < (lowLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - lowLsnBitmap + 1;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
if(pointer <= 72)
|
||||
{
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(getMessage().getInt(BYTE, pointer)));
|
||||
pointer += 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Create a placeholder for the talkgroup since we don't know the value
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer += 8;
|
||||
}
|
||||
|
||||
//Process voice LSNs 9 - 16
|
||||
if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
int highLsnBitmap = pointer;
|
||||
|
||||
pointer += 8;
|
||||
|
||||
for(int x = highLsnBitmap; x < (highLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - highLsnBitmap + 1 + 8;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
if(pointer <= 72)
|
||||
{
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(getMessage().getInt(BYTE, pointer)));
|
||||
pointer += 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Create a placeholder for the talkgroup since we don't know the value
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer += 8;
|
||||
}
|
||||
|
||||
//Process Radio IDs - first bit in radio options byte is set to indicate more activity
|
||||
if(pointer <= 72 && getMessage().get(pointer))
|
||||
{
|
||||
pointer += 8;
|
||||
|
||||
//If we have the data revert channel bitmap ...
|
||||
if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0)
|
||||
{
|
||||
int lowDataLsnBitmap = pointer;
|
||||
|
||||
pointer += 8;
|
||||
|
||||
for(int x = lowDataLsnBitmap; x < (lowDataLsnBitmap + 8); x++)
|
||||
{
|
||||
int lsn = x - lowDataLsnBitmap + 1;
|
||||
|
||||
if(getMessage().get(x))
|
||||
{
|
||||
if(pointer <= 64)
|
||||
{
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRRadio.createTo(getMessage().getInt(TWO_BYTES, pointer)));
|
||||
pointer += 16;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Create a placeholder for the radio since we don't know the value
|
||||
mActiveLsnMap.put(lsn, new DMRLsn(lsn));
|
||||
mActiveIdentifierMap.put(lsn, DMRRadio.createTo(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mActiveIdentifierMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Segment indicator for system status message values that are fragmented across multiple system status
|
||||
@@ -301,7 +343,15 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
@Override
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getRestChannel().getLogicalChannelNumbers();
|
||||
Set<Integer> lcnSet = new HashSet<>();
|
||||
lcnSet.add(getRestChannel().getChannelNumber());
|
||||
|
||||
for(DMRLsn lsn: getActiveLsnMap().values())
|
||||
{
|
||||
lcnSet.add(lsn.getChannelNumber());
|
||||
}
|
||||
|
||||
return lcnSet.stream().mapToInt(v -> v).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,6 +363,10 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
getRestChannel().apply(timeslotFrequencies);
|
||||
for(DMRLsn lsn: getActiveLsnMap().values())
|
||||
{
|
||||
lsn.apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -118,7 +118,7 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
sb.append(" ENCRYPTED");
|
||||
}
|
||||
|
||||
sb.append(" CLEAR - RETURN TO ").append(getMoveToChannel());
|
||||
sb.append(" CLEAR - RETURN TO").append(getMoveToChannel());
|
||||
sb.append(" FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationId());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
|
||||
@@ -151,6 +151,10 @@ public class AdjacentSiteInformation extends Announcement
|
||||
}
|
||||
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
sb.append(" ").append(getSystemIdentityCode().getModel());
|
||||
sb.append(" NEIGHBOR NETWORK:").append(getNeighborSystemIdentityCode().getNetwork());
|
||||
sb.append(" SITE:").append(getNeighborSystemIdentityCode().getSite());
|
||||
|
||||
@@ -105,6 +105,7 @@ public class TalkgroupVoiceChannelGrant extends ChannelGrant
|
||||
sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationTalkgroup());
|
||||
sb.append(" ").append(getChannel());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMe
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
|
||||
@@ -109,6 +112,15 @@ public class LCMessageFactory
|
||||
|
||||
LCOpcode opcode = FullLCMessage.getOpcode(message);
|
||||
|
||||
//Some Hytera Tier-3 systems use a zero-valued mask when the FLC is carried in the voice header or terminator
|
||||
//and in the same transmission it uses the standard mask when carried across the voice frames. It doesn't make
|
||||
//sense, but maybe it's a bug in their software implementation?
|
||||
if(!valid && opcode == LCOpcode.FULL_STANDARD_GROUP_VOICE_CHANNEL_USER)
|
||||
{
|
||||
//Retry the RS(12,9,4) with a mask value of zero
|
||||
valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, 0);
|
||||
}
|
||||
|
||||
FullLCMessage flc = null;
|
||||
|
||||
switch(opcode)
|
||||
@@ -128,6 +140,15 @@ public class LCMessageFactory
|
||||
case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
|
||||
flc = new MotorolaGroupVoiceChannelUser(message, timestamp, timeslot);
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER:
|
||||
flc = new CapacityMaxVoiceChannelUser(message, timestamp, timeslot);
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS:
|
||||
flc = new CapacityMaxTalkerAlias(message, timestamp, timeslot);
|
||||
break;
|
||||
case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION:
|
||||
flc = new CapacityMaxTalkerAliasContinuation(message, timestamp, timeslot);
|
||||
break;
|
||||
case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER:
|
||||
flc = new CapacityPlusEncryptedVoiceChannelUser(message, timestamp, timeslot);
|
||||
break;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user