Compare commits

..

30 Commits

Author SHA1 Message Date
Denny Sheirer
aeeed71231 #1834 DMR CapMax Adv mode call events can have 'No Frequency'. Updated message class to ensure all 4x channels get nominated for timeslot frequency enrichment where previously the timeslot 2 channels might not receive a frequency enrichment. Also protected against bad logging for an UnknownCSBKMessage that gets constructed and CRC correction causing the message to have a valid channel grant opcode. (#1835)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 04:42:50 -05:00
Denny Sheirer
f6c4cc8a5f 1810 P25 Voice frame list concurrent modification error. Added synchronized to method to deconflict when the audio module or the binary frame recorder can access the message and not step on each other. Issue was noted in the phase 2 module, but update the phase 1 module as well to protect against the issue. (#1833)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 03:40:57 -05:00
Denny Sheirer
d019388034 1826 Vector Oscillator fails to generate MPT1327 correlator reference samples on SIMD-512 machines. (#1832)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 10:39:46 -05:00
Denny Sheirer
e125d6a452 #1830 Health & Diagnostic Monitor feature. (#1831)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 10:05:51 -05:00
Denny Sheirer
6b5a1fb827 #1802 Traffic channels stuck in TEARDOWN. Resolves: (#1829)
1. Tuner channel processing runnables were being interruptibly shutdown, preventing the channel from clearing any owned locks (ie DMRTrafficChannelManager) causing thread deadlock and eventually causing out of memory situation or tying up all threads in the thread pool to the point that the application stopped processing any data.

2. DMR Traffic Channel Manager was erroneously processing non-owned traffic channel events.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 06:05:19 -05:00
Denny Sheirer
0491b08818 #1827 USB Hotplug support callback can block the UI thread causing blank white screen. (#1828)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 03:38:12 -05:00
Denny Sheirer
7b9adc1da2 #1824 Adds diagnostic report generation to File>>Reports menu to log thread dumps and generate a processing diagnostic/status report. (#1825)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-04 09:59:53 -05:00
Denny Sheirer
3acaea0019 #1821 Resolves issue with SDRPlay IF Gain auto/manual modes and slider adjustment. (#1822)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 16:12:01 -05:00
Denny Sheirer
be02393f65 #1819 SDRPlay RSP1B tuner is now constructed correctly. (#1820)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 15:28:39 -05:00
Denny Sheirer
614bf72f01 SDRPlay API version 3.14 & RSP1B (#1817)
* #1812 sdrplay adds support for RSP1B and changes gain to expose LNA and Baseband gain settings for all tuners.

* Updates build to 0.6.1 beta 1.

---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 10:18:05 -05:00
Peter Hicks
bed28b82ab Fix the channel power meter so that the heading displays without truncation (#1814) 2024-01-30 04:10:49 -05:00
Peter Hicks
c047d48d91 Merge pull request #1813
* OSM Tileserver now supports TLSv1.3, so we should use this in prefere…
2024-01-30 04:08:46 -05:00
Moto
5222bffce4 Merge pull request #1808
* Inherit preferred tuner configuration to P25 traffic channels
2024-01-30 04:05:19 -05:00
Denny Sheirer
42e7a49d37 #1805 DMR details tab enhancements and opcode cleanup. (#1806)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-01-23 16:38:18 -05:00
Denny Sheirer
686bddf7d5 #1799 DMR Cap+ & CapMax now track events correctly. Updated Cap+ events to include channel descriptor and frequencies. Corrected Capacity Max talker aliases to combine base and continuation alias fragments into a unified alias identifier. (#1801)
Critical: resolved a thread deadlock issue when the DMR multi-frequency channel rotation thread deadlocks against the UI thread when accessing the current channel's frequency from the frequency controller.  This thread deadlock may be the source of multiple users reporting OutOfMemory crashes when running any DMR channel with multiple control frequencies where the decoder was rolling through each frequency checking for activity.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-01-21 07:47:37 -05:00
Denny Sheirer
1c420b8eee #1787 Resolves issue with channel rotation and rest channel reallocation for Capacity Plus systems. (#1798)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-19 10:23:27 -05:00
Denny Sheirer
e2a0325d07 #1787 Adds CapMax talker alias continuation FLCO. Resolves issue with Con+ control channel SLCO message detection. (#1796)
#1787 Adds Capacity Max talker alias FLCO.  Fixes standard FLC Talker Alias using ISO7 format.  Updates channel details to list the tier 3 neighbors in the details.
#1787 Updates DMR capacity max event handling to better show events.  Adds support for voice channel update for both Open System and Advantage modes.  May cause issues with Cap+ or Con+ due to changes ... needs more testing.
#1787 DMR capacity max group voice update handling.

Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-18 11:48:25 -05:00
Denny Sheirer
9d03c151af #1787 DMR Decoder enhancements for RAS enabled systems. Adds support for CSBKO:33 and FLCO:16 (#1794)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-13 09:51:59 -05:00
crsawyer
3c18bfdc81 P25-P2 Encryption Synchronization ReSync (#1792)
* resync ess processor on PTT, END_PTT, HANGTIME, or IDLE

* Re-order of AbstractVoiceTimeslot. Voice2 should be processed first, as any accumulated ESS information applies to the following Voice slots

* Update P25 MBE recorders to include synchronized encryption information

---------

Co-authored-by: Sawyer, Caleb (US) - ISR <Caleb.Sawyer@L3Harris.com>
2024-01-12 09:49:49 -05:00
Denny Sheirer
d89ba65238 Merge pull request #1791 from tadscottsmith/openmhz-reconnect-error
#1791 Resolves OpenMHz Broadcaster Auto-Reconnection Issue
2024-01-12 08:36:34 -05:00
Denny Sheirer
d2cf2ee7b5 Merge pull request #1793 from DSheirer/1787-dmr-mask-manager-update
#1787 Enhance DMR CSBK CRC Calculations - Auto-Detect Alternate CRC Mask Values
2024-01-12 07:53:01 -05:00
Dennis Sheirer
7a03331955 #1787 Enhances DMR CSBK message processing by auto-detecting alternate CRC mask values employed by the DMR network. 2024-01-12 07:49:08 -05:00
tadscottsmith
7c5ef3803a Fix typo. 2024-01-10 19:06:43 -06:00
tadscottsmith
6f3e7c86e5 Fix OpenMHz Reconnection Issue. 2024-01-10 14:58:46 -06:00
Denny Sheirer
fc51de2b4f Merge pull request #1786 from DSheirer/1783-l3h-p2-talker-alias-part-2
#1783 L3H P25P2 Talker Alias Guard Against Negative Length Aliases
2024-01-10 02:48:28 -05:00
Dennis Sheirer
e1e75710cb #1783 L3H Talker Alias guard against negative length aliases 2024-01-10 02:46:22 -05:00
Denny Sheirer
87ce08e9b9 Merge pull request #1784 from DSheirer/1783-l3h-p2-message-parse-error
#1783 P25P2 L3Harris Talker Alias Index Out Of Bounds Exception
2024-01-08 03:58:17 -05:00
Dennis Sheirer
1a5c7521a5 #1783 P25P2 L3Harris Talker Alias - resolves issue with trying to parse too many bits. 2024-01-08 03:56:10 -05:00
Denny Sheirer
b54e94dfbe Merge pull request #1773 from DSheirer/1772-patch-group-streaming
1772 Patch Group Streaming Preference (Patch Group vs Individual Talkgroups)
2023-12-17 06:58:09 -05:00
Dennis Sheirer
19977d5f4d 1772 Adds preference for patch group streaming to stream patch group calls as either a)patch group, or b)each individual talkgroup or radio that forms the patch group. 2023-12-17 06:55:23 -05:00
217 changed files with 14902 additions and 2336 deletions

View File

@@ -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.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -1,6 +1,6 @@
#
# ******************************************************************************
# Copyright (C) 2014-2023 Dennis Sheirer
# Copyright (C) 2014-2024 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -28,4 +28,4 @@
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.6.0
projectVersion=0.6.1-beta-1

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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
*/

View File

@@ -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;
}
}

View File

@@ -54,6 +54,7 @@
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
@@ -69,6 +70,7 @@
import java.util.concurrent.TimeUnit;
/**
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
*
@@ -115,14 +117,15 @@
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
/**
* OpenMHz API does not currently expose a test method.
* TODO: FIX THIS
*/
if(response == "OK")// && response.toLowerCase().startsWith("<head><title>502 bad gateway</title></head>"))
if(response == "OK")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if (response == "No Response")
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Error connecting to OpenMHz server [Server not found or not reachable]");
}
else
{
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
@@ -170,8 +173,6 @@
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
* the audio processor thread will persistently invoke this method to attempt a reconnect.
*
* OpenMHz does not have a test API endpoint, so we look for the incomplete call response.
*/
private boolean connected()
{
@@ -183,14 +184,21 @@
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response != null && response == "200")
if(response == "OK")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if (response == "No Response")
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Error reconnecting to OpenMHz server [Server not found or not reachable]");
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error reconnecting to OpenMHz server [" + response + "]");
}
}
return getBroadcastState() == BroadcastState.CONNECTED;
@@ -565,8 +573,8 @@
try
{
HttpResponse<String> response = httpClient.send(request, responseHandler);
String responseBody = response.body();
HttpResponse<String> response = httpClient.send(request, responseHandler);
if (response.statusCode() == 200)
{
return "OK";
@@ -583,8 +591,16 @@
return "No Response";
}
catch(Exception e)
{
return e.getLocalizedMessage();
{
Throwable throwableCause = e.getCause();
if(throwableCause instanceof ConnectException)
{
return "No Response";
}
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
return "Uknown Exception";
}
}

View File

@@ -125,7 +125,7 @@ public class MBECallSequenceConverter
try
{
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
}
catch(IOException ioe)
{

View File

@@ -307,8 +307,8 @@ public class BinaryMessage extends BitSet
/**
* Overrides the parent method which returns a BitSet so that we can return a BinaryMessage
* @param from
* @param to
* @param from inclusive
* @param to exclusive
* @return
*/
@Override
@@ -1198,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);

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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()
{
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -43,6 +43,7 @@ import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.monitor.DiagnosticMonitor;
import io.github.dsheirer.monitor.ResourceMonitor;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -125,6 +126,7 @@ public class SDRTrunk implements Listener<TunerEvent>
private AudioStreamingManager mAudioStreamingManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
private ControllerPanel mControllerPanel;
private DiagnosticMonitor mDiagnosticMonitor;
private IconModel mIconModel = new IconModel();
private PlaylistManager mPlaylistManager;
private SettingsManager mSettingsManager;
@@ -187,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);
}
@@ -411,6 +419,46 @@ public class SDRTrunk implements Listener<TunerEvent>
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenuItem processingStatusReportMenuItem = new JMenuItem("Processing Diagnostic Report");
processingStatusReportMenuItem.addActionListener(e -> {
try
{
Path path = mDiagnosticMonitor.generateProcessingDiagnosticReport("User initiated diagnostic report");
JOptionPane.showMessageDialog(mMainGui, "Report created: " +
path.toString(), "Processing Status Report Created", JOptionPane.INFORMATION_MESSAGE);
}
catch(IOException ioe)
{
mLog.error("Error creating processing status report file", ioe);
JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " +
"see application log for details.", "Processing Status Report Failed", JOptionPane.ERROR_MESSAGE);
}
});
JMenuItem threadDumpReportMenuItem = new JMenuItem("Thread Dump Report");
threadDumpReportMenuItem.addActionListener(e -> {
try
{
Path path = mDiagnosticMonitor.generateThreadDumpReport();
JOptionPane.showMessageDialog(mMainGui, "Report created: " +
path.toString(), "Thread Dump Report Created", JOptionPane.INFORMATION_MESSAGE);
}
catch(IOException ioe)
{
mLog.error("Error creating thread dump report file", ioe);
JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " +
"see application log for details.", "Thread Dump Report Failed", JOptionPane.ERROR_MESSAGE);
}
});
JMenu diagnosticMenu = new JMenu(("Reports"));
diagnosticMenu.add(processingStatusReportMenuItem);
diagnosticMenu.add(threadDumpReportMenuItem);
fileMenu.add(diagnosticMenu);
fileMenu.add(new JSeparator(JSeparator.HORIZONTAL));
JMenuItem exitMenu = new JMenuItem("Exit");
exitMenu.addActionListener(event -> {
processShutdown();
@@ -578,6 +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,

View File

@@ -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)"));
}

View File

@@ -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:

View File

@@ -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"),

View File

@@ -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));

View File

@@ -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;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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

View File

@@ -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()
{
}
}

View File

@@ -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,17 +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;
@@ -66,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;
@@ -80,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;
@@ -91,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;
@@ -105,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.
@@ -142,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.
*/
@@ -150,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,
@@ -186,7 +276,6 @@ public class DMRDecoderState extends TimeslotDecoderState
resetState();
mCurrentFrequency = 0;
mDetectedCallEventsMap.clear();
}
/**
@@ -206,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)
{
@@ -232,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);
}
}
@@ -297,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())
@@ -321,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())
@@ -336,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())
@@ -361,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())
@@ -373,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())
@@ -385,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())
@@ -397,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())
@@ -409,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())
@@ -418,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())
@@ -503,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);
}
@@ -520,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:
@@ -539,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:
@@ -579,7 +676,7 @@ public class DMRDecoderState extends TimeslotDecoderState
LCMessage lcMessage = terminator.getLCMessage();
if(lcMessage.isValid())
if(isValid(lcMessage))
{
processLinkControl(lcMessage, true);
}
@@ -592,7 +689,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
LCMessage lcMessage = voiceHeader.getLCMessage();
if(lcMessage.isValid())
if(isValid(lcMessage))
{
processLinkControl(lcMessage, false);
}
@@ -745,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())
{
@@ -768,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;
@@ -867,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())
@@ -912,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())
@@ -920,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())
@@ -968,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)
@@ -1000,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
@@ -1090,6 +1104,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(mCurrentCallEvent != null)
{
mCurrentCallEvent.setDetails(ep.getDetails());
broadcast(mCurrentCallEvent);
}
}
break;
@@ -1104,6 +1119,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(cpgvcu.getTalkgroup());
}
@@ -1121,6 +1137,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(cpgvcu.getTalkgroup());
}
@@ -1133,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)
{
@@ -1142,6 +1230,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(cpuo4.getTalkgroup());
}
@@ -1158,6 +1247,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(hgvcu.getTalkgroup());
}
@@ -1182,6 +1272,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(huuvcu.getTargetRadio());
}
@@ -1201,6 +1292,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(gvcu.getTalkgroup());
}
@@ -1220,6 +1312,7 @@ public class DMRDecoderState extends TimeslotDecoderState
if(isTerminator)
{
closeCurrentCallEvent(message.getTimestamp());
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(uuvcu.getTargetRadio());
}
@@ -1309,8 +1402,6 @@ public class DMRDecoderState extends TimeslotDecoderState
.identifiers(getIdentifierCollection().copyOf())
.timeslot(getTimeslot())
.build();
broadcast(mCurrentCallEvent);
}
else
{
@@ -1321,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()));
@@ -1347,6 +1439,8 @@ public class DMRDecoderState extends TimeslotDecoderState
broadcast(mCurrentCallEvent);
mCurrentCallEvent = null;
}
getIdentifierCollection().remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
}
@Override
@@ -1367,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:
@@ -1387,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())
{
@@ -1398,9 +1491,4 @@ public class DMRDecoderState extends TimeslotDecoderState
public void init()
{
}
@Override
public void stop()
{
}
}

View File

@@ -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.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;
@@ -66,13 +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
@@ -80,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())
{
@@ -89,6 +93,17 @@ 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
*/
@@ -128,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)
{
@@ -140,28 +155,15 @@ 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);
@@ -243,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
*/

View File

@@ -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");
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -24,20 +24,38 @@ 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.
* 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, ResidualCrcMaskTracker> mCsbkResidualTrackerMap = new TreeMap<>();
private int mDominantMask = 0;
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
@@ -46,35 +64,27 @@ public class DmrCrcMaskManager
{
}
/**
* Creates a deep copy (ie clone) of all tracked values
* @return list of tracker clones.
*/
public List<ResidualCrcMaskTracker> cloneTrackers()
{
List<ResidualCrcMaskTracker>trackers = new ArrayList<>();
for(ResidualCrcMaskTracker tracker: mCsbkResidualTrackerMap.values())
{
trackers.add(tracker.clone());
}
return trackers;
}
/**
* Logs the current tracked CRC values and counts.
*/
public void log()
{
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
Collections.sort(trackers);
StringBuilder sb = new StringBuilder();
sb.append("DMR CRC Mask Manager - Current Tracked Values\n");
for(ResidualCrcMaskTracker tracker: trackers)
sb.append("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.getCount());
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");
}
@@ -83,104 +93,139 @@ public class DmrCrcMaskManager
/**
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
* if so, attempts to correct the message by using the alternate mask value once the alternate mask value is
* detected to be used consistently enough.
* @param csbk to re-check.
* 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 residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
int alternateMask = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
if(mCsbkResidualTrackerMap.containsKey(residual))
Map<Integer,MaskTracker> map;
Set<MaskTracker> sortedSet;
if(csbk.getTimeslot() == 1)
{
mCsbkResidualTrackerMap.get(residual).increment();
map = mCsbkTrackerMapTS1;
sortedSet = mSortedTrackersTS1;
}
else
{
mCsbkResidualTrackerMap.put(residual, new ResidualCrcMaskTracker(residual));
map = mCsbkTrackerMapTS2;
sortedSet = mSortedTrackersTS2;
}
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
Collections.sort(trackers);
if(trackers.size() > 5)
if(map.containsKey(alternateMask))
{
mCsbkResidualTrackerMap.remove(trackers.get(0).getTrackedValue());
}
MaskTracker matchingTracker = map.get(alternateMask);
matchingTracker.increment(csbk.getTimestamp());
if(trackers.size() > 0)
{
ResidualCrcMaskTracker dominant = trackers.get(trackers.size() - 1);
if(dominant.isValid())
if(matchingTracker.isValid())
{
mDominantMask = dominant.getTrackedValue();
csbk.checkCRC(matchingTracker.getTrackedValue());
}
}
if(mDominantMask != 0)
else
{
csbk.checkCRC(mDominantMask);
//
// if(csbk.isValid())
// {
// LOGGER.info("CSBK fixed using alternate mask: " + Integer.toHexString(mDominantMask).toUpperCase());
// }
//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);
}
}
}
}
/**
* Residual CRC mask value tracker. Tracks the number of observances of a residual CRC calculated value to detect
* when an alternate CRC mask pattern is employed and automatically correct CRC values for messages.
* 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 ResidualCrcMaskTracker implements Comparable<ResidualCrcMaskTracker>
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 mCount;
private int mObservationCount;
private int mStalenessCount;
private long mLastUpdated;
/**
* Constructs an instance for the specified residual, sets the count to one and sets the last update to now.
* 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 ResidualCrcMaskTracker(int residual)
public MaskTracker(int residual, long timestamp)
{
mTrackedValue = residual;
mCount = 1;
mLastUpdated = System.currentTimeMillis();
mObservationCount = 1;
mLastUpdated = timestamp;
}
/**
* Create a deep copy of this instance.
* @return cloned instance.
*/
public ResidualCrcMaskTracker clone()
{
ResidualCrcMaskTracker clone = new ResidualCrcMaskTracker(getTrackedValue());
clone.mCount = mCount;
clone.mLastUpdated = mLastUpdated;
return clone;
}
/**
* Count of number of times this residual has been observed.
* Count of observations for this mask value.
* @return count
*/
public int getCount()
public int getObservationCount()
{
return mCount;
return mObservationCount;
}
/**
* Indicates that the residual value has been observed at least 3x times indicating that it is possibly valid.
* @return
* 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 mCount >= 3;
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;
}
/**
@@ -192,38 +237,44 @@ public class DmrCrcMaskManager
}
/**
* Residual tracked value.
* @return residual
* Tracked alternate mask value.
* @return tracked mask value
*/
public int getTrackedValue()
{
return mTrackedValue;
}
public void increment()
/**
* 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)
{
mCount++;
//Prevent rollover by keeping value at max integer value.
if(mCount < 0)
mStalenessCount = 0;
mObservationCount++;
//Detect integer rollover and apply max integer value.
if(mObservationCount < 0)
{
mCount = Integer.MAX_VALUE;
mObservationCount = Integer.MAX_VALUE;
}
mLastUpdated = System.currentTimeMillis();
mLastUpdated = timestamp;
}
/**
* Custom sort order by count and then by last updated timestamp.
* Custom (reversed) sort order by largest count and then by latest updated timestamp.
* @param o the object to be compared.
* @return
* @return comparison value.
*/
@Override
public int compareTo(ResidualCrcMaskTracker o)
public int compareTo(MaskTracker o)
{
int comparison = Integer.compare(getCount(), o.getCount());
//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());
comparison = Long.compare(getLastUpdated(), o.getLastUpdated()) * -1;
}
return comparison;

View File

@@ -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;
}
}

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -34,6 +34,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTraff
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;
@@ -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;

View File

@@ -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"),

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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());

View File

@@ -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();
}

View File

@@ -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;
@@ -137,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;

View File

@@ -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
@@ -45,6 +45,10 @@ public enum LCOpcode
FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 0, "GROUP VOICE CHANNEL USER"),
FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 4, "WAN GROUP VOICE CHANNEL USER"),
FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 16, "CAPMAX GROUP VOICE CHANNEL USER"),
FULL_CAPACITY_MAX_TALKER_ALIAS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 20, "CAPMAX TALKER ALIAS"),
FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION(Vendor.MOTOROLA_CAPACITY_PLUS, true, 21, "CAPMAX TALKER ALIAS CONTINUATION"),
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"),
//Observed on Cap+ Multi-Site System during an encrypted voice call

View File

@@ -0,0 +1,92 @@
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier;
import java.util.ArrayList;
import java.util.List;
/**
* Capacity Max Talker Alias (FLCO 20 / FID 0x10)
* <p>
* See also: FLCO 21 - Talker Alias Continuation
*/
public class CapacityMaxTalkerAlias extends CapacityPlusVoiceChannelUser
{
private static final int[] ENCODING_TYPE = new int[]{16, 17, 18};
private static final int[] LENGTH = new int[]{19, 20, 21, 22};
private static final int ALIAS_START = 24;
private static final int ALIAS_END = ALIAS_START + (6 * 8); //6 characters?
private DmrTalkerAliasIdentifier mTalkerAliasIdentifier;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance.
*
* @param message for the link control payload
* @param timestamp
* @param timeslot
*/
public CapacityMaxTalkerAlias(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
public int getLength()
{
return getMessage().getInt(LENGTH);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS:").append(getTalkerAliasIdentifier());
sb.append(" LENGTH:").append(getLength());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Talker alias identifier
*
* @return identifier
*/
public DmrTalkerAliasIdentifier getTalkerAliasIdentifier()
{
if(mTalkerAliasIdentifier == null)
{
String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim();
mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias);
}
return mTalkerAliasIdentifier;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,99 @@
/*
* *****************************************************************************
* 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.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier;
import java.util.ArrayList;
import java.util.List;
/**
* Capacity Max Talker Alias Continuation (FLCO 21 / FID 0x10)
*/
public class CapacityMaxTalkerAliasContinuation extends CapacityPlusVoiceChannelUser
{
private static final int ALIAS_START = 16;
private static final int ALIAS_END = ALIAS_START + (7 * 8);
private DmrTalkerAliasIdentifier mTalkerAliasIdentifier;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance.
*
* @param message for the link control payload
* @param timestamp
* @param timeslot
*/
public CapacityMaxTalkerAliasContinuation(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS CONTINUED:").append(getTalkerAliasIdentifier());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Talker alias identifier
* @return identifier
*/
public DmrTalkerAliasIdentifier getTalkerAliasIdentifier()
{
if(mTalkerAliasIdentifier == null)
{
String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim();
mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias);
}
return mTalkerAliasIdentifier;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,102 @@
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
/**
* Capacity Max Voice Channel User (FLCO 16 / 0x10)
*/
public class CapacityMaxVoiceChannelUser extends CapacityPlusVoiceChannelUser
{
private static final int[] TALKGROUP = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71};
private TalkgroupIdentifier mTalkgroup;
private RadioIdentifier mRadio;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance.
*
* @param message for the link control payload
* @param timestamp
* @param timeslot
*/
public CapacityMaxVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA CAPMAX GROUP VOICE CHANNEL USER FM:");
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Talkgroup TO ID
*/
public TalkgroupIdentifier getTalkgroup()
{
if(mTalkgroup == null)
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP));
}
return mTalkgroup;
}
/**
* Radio FROM ID
*/
public RadioIdentifier getRadio()
{
if(mRadio == null)
{
mRadio = DMRRadio.createFrom(getMessage().getInt(RADIO));
}
return mRadio;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
mIdentifiers.add(getRadio());
}
return mIdentifiers;
}
}

View File

@@ -88,7 +88,8 @@ public abstract class ShortLCMessage extends LCMessage implements IMessage
{
Vendor vendor = null;
if(binaryMessage.get(VENDOR_FLAG))
//If the first bit is set, consider this a vendor variant, unless the opcode is 10 for Connect Plus Control Channel.
if(binaryMessage.get(VENDOR_FLAG) && binaryMessage.getInt(OPCODE) != 10)
{
vendor = Vendor.fromValue(binaryMessage.getInt(VENDOR));
}

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.module.decode.dmr.message.data.mbc;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory;
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
@@ -34,21 +35,35 @@ public class MBCAssembler
private MBCHeader mTS2Header;
private List<MBCContinuationBlock> mTS1ContinuationBlocks = new ArrayList<>();
private List<MBCContinuationBlock> mTS2ContinuationBlocks = new ArrayList<>();
private boolean mIgnoreCrcChecksums;
/**
* Constructs an instance
*/
public MBCAssembler()
public MBCAssembler(boolean ignoreCrcChecksums)
{
mIgnoreCrcChecksums = ignoreCrcChecksums;
}
/**
* 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();
}
/**
* Processes the MBC header
* @param header to process
*/
public void process(MBCHeader header)
{
if(header != null && header.isValid())
if(header != null && isValid(header))
{
reset(header.getTimeslot());

View File

@@ -148,6 +148,15 @@ public enum DecodeEventType
DATA_CALLS.contains(this) || REGISTRATION.contains(this);
}
/**
* Indicates if this is a call event.
* @return true if this is a call event type.
*/
public boolean isCallEvent()
{
return VOICE_CALLS.contains(this) || VOICE_CALLS_ENCRYPTED.contains(this) || DATA_CALLS.contains(this);
}
/**
* Label or pretty value for the element
* @return label

View File

@@ -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
@@ -53,20 +53,7 @@ public class Fleetsync2DecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
@Override
public void receive(IMessage message)

View File

@@ -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
@@ -150,20 +150,5 @@ public class LJ1200DecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
}

View File

@@ -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
@@ -586,19 +586,7 @@ public class LTRNetDecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
/**
* Resets the decoder state after a call or other decode event

View File

@@ -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
@@ -153,19 +153,7 @@ public class LTRStandardDecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
/**
* Performs a temporal reset following a call or other decode event

View File

@@ -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
@@ -58,22 +58,7 @@ public class MDCDecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
@Override
public void receive(IMessage message)

View File

@@ -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
@@ -220,6 +220,7 @@ public class MPT1327DecoderState extends DecoderState
@Override
public void start()
{
super.start();
//Send call start event for traffic channels to unsquelch the audio. Decoded return to channel message
//or fade timeout expire will end the call event.
if(mChannelType == ChannelType.TRAFFIC)
@@ -228,18 +229,8 @@ public class MPT1327DecoderState extends DecoderState
broadcast(new DecoderStateEvent(this, Event.START, State.CALL));
}
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
protected void resetState()
{

View File

@@ -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
@@ -301,6 +301,13 @@ public class MPT1327TrafficChannelManager extends TrafficChannelManager implemen
*/
public class TrafficChannelTeardownMonitor implements Listener<ChannelEvent>
{
/**
* Process channel events from the ChannelProcessingManager to account for owned child traffic channels.
* Note: this method sees events for ALL channels and not just MPT1327 channels managed by this instance.
*
* @param channelEvent to process
*/
@Override
public synchronized void receive(ChannelEvent channelEvent)
{

View File

@@ -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
@@ -309,6 +309,10 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(frequency);
if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
{
sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
}
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
@@ -356,6 +360,10 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(frequency);
if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
{
sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
}
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
@@ -455,6 +463,10 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(frequency);
if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
{
sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
}
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
@@ -517,6 +529,10 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(frequency);
if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
{
sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
}
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
@@ -778,6 +794,12 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
}
}
/**
* Process channel events from the ChannelProcessingManager to account for owned child traffic channels.
* Note: this method sees events for ALL channels and not just P25 channels managed by this instance.
*
* @param channelEvent to process
*/
@Override
public synchronized void receive(ChannelEvent channelEvent)
{

View File

@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser;
@@ -124,6 +127,10 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
{
flush();
}
else if (message instanceof HDUMessage)
{
process((HDUMessage)message);
}
}
private void process(TDULinkControlMessage tdulc)
@@ -228,4 +235,21 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
mCallSequence.setEncryptionSyncParameters(parameters);
}
}
private void process(HDUMessage hduMessage)
{
if(mCallSequence == null)
{
mCallSequence = new MBECallSequence(PROTOCOL);
}
HeaderData hd = hduMessage.getHeaderData();
if (hd.isEncryptedAudio())
{
mCallSequence.setEncrypted(true);
Phase2EncryptionSyncParameters esp = new Phase2EncryptionSyncParameters((EncryptionKeyIdentifier)hd.getEncryptionKey(), hd.getMessageIndicator());
mCallSequence.setEncryptionSyncParameters(esp);
}
}
}

View File

@@ -27,6 +27,7 @@ import io.github.dsheirer.audio.codec.mbe.MBECallSequence;
import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
@@ -153,6 +154,10 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
{
process((MacMessage)message);
}
else if (message instanceof EncryptionSynchronizationSequence)
{
processEss((EncryptionSynchronizationSequence)message);
}
}
/**
@@ -382,5 +387,18 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
mLog.warn("Expected push-to-talk structure but found: " + mac.getClass());
}
}
private void processEss(EncryptionSynchronizationSequence ess)
{
if(mCallSequence == null)
{
mCallSequence = new MBECallSequence(PROTOCOL);
}
if (ess.isEncrypted())
{
mCallSequence.setEncrypted(true);
mCallSequence.setEncryptionSyncParameters(ess);
}
}
}
}

View File

@@ -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
@@ -1948,6 +1948,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
@Override
public void start()
{
super.start();
mPatchGroupManager.clear();
//Change the default (45-second) traffic channel timeout to 1 second
@@ -1965,6 +1966,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
@Override
public void stop()
{
super.stop();
mPatchGroupManager.clear();
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 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,23 +14,21 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.message.ldu;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* LDU voice frame
*/
public abstract class LDUMessage extends P25Message
{
private final static Logger mLog = LoggerFactory.getLogger(LDUMessage.class);
public static final int IMBE_FRAME_1 = 0;
public static final int IMBE_FRAME_2 = 144;
public static final int IMBE_FRAME_3 = 328;
@@ -41,10 +38,16 @@ public abstract class LDUMessage extends P25Message
public static final int IMBE_FRAME_7 = 1064;
public static final int IMBE_FRAME_8 = 1248;
public static final int IMBE_FRAME_9 = 1424;
public static final int[] LOW_SPEED_DATA = {1392, 1393, 1394, 1395, 1396, 1397, 1398, 1399, 1408, 1409, 1410, 1411,
1412, 1413, 1414, 1415};
private List<byte[]> mIMBIFrames;
public static final int[] LOW_SPEED_DATA = {1392, 1393, 1394, 1395, 1396, 1397,
1398, 1399, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415};
/**
* Constructs an instance
* @param message with data
* @param nac code
* @param timestamp for the message
*/
public LDUMessage(CorrectedBinaryMessage message, int nac, long timestamp)
{
super(message, nac, timestamp);
@@ -58,6 +61,10 @@ public abstract class LDUMessage extends P25Message
return getMessage().getHex(LOW_SPEED_DATA, 4);
}
/**
* Base message used by subclass implementations.
* @return base message
*/
public String getMessageStub()
{
StringBuilder sb = new StringBuilder();
@@ -70,24 +77,25 @@ public abstract class LDUMessage extends P25Message
}
/**
* Returns a 162 byte array containing 9 IMBE voice frames of 18-bytes
* (144-bits) each. Each frame is intact as transmitted and requires
* deinterleaving, error correction, derandomizing, etc.
* Returns a 162 byte array containing 9 IMBE voice frames of 18-bytes* (144-bits) each. Each frame is intact as
* transmitted and requires deinterleaving, error correction, derandomizing, etc.
*/
public List<byte[]> getIMBEFrames()
public synchronized List<byte[]> getIMBEFrames()
{
List<byte[]> frames = new ArrayList<byte[]>();
if(mIMBIFrames == null)
{
mIMBIFrames = new ArrayList<>();
mIMBIFrames.add(getMessage().get(IMBE_FRAME_1, IMBE_FRAME_1 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_2, IMBE_FRAME_2 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_3, IMBE_FRAME_3 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_4, IMBE_FRAME_4 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_5, IMBE_FRAME_5 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_6, IMBE_FRAME_6 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_7, IMBE_FRAME_7 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_8, IMBE_FRAME_8 + 144).toByteArray());
mIMBIFrames.add(getMessage().get(IMBE_FRAME_9, IMBE_FRAME_9 + 144).toByteArray());
}
frames.add(getMessage().get(IMBE_FRAME_1, IMBE_FRAME_1 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_2, IMBE_FRAME_2 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_3, IMBE_FRAME_3 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_4, IMBE_FRAME_4 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_5, IMBE_FRAME_5 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_6, IMBE_FRAME_6 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_7, IMBE_FRAME_7 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_8, IMBE_FRAME_8 + 144).toByteArray());
frames.add(getMessage().get(IMBE_FRAME_9, IMBE_FRAME_9 + 144).toByteArray());
return frames;
return mIMBIFrames;
}
}

View File

@@ -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
@@ -1161,6 +1161,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
@Override
public void start()
{
super.start();
mPatchGroupManager.clear();
//Change the default (45-second) traffic channel timeout to 1 second
@@ -1178,6 +1179,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
@Override
public void stop()
{
super.stop();
mPatchGroupManager.clear();
}
}

View File

@@ -101,11 +101,30 @@ public class P25P2MessageProcessor implements Listener<IMessage>
mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier);
}
switch(macMessage.getMacPduType())
{
case MAC_1_PTT:
case MAC_2_END_PTT:
case MAC_6_HANGTIME:
case MAC_3_IDLE:
if (timeslot.getTimeslot() == 0)
{
mESSProcessor0.reset();
}
else
{
mESSProcessor1.reset();
}
break;
}
mMessageListener.receive(macMessage);
}
}
else if(timeslot instanceof AbstractVoiceTimeslot)
{
mMessageListener.receive(timeslot);
if(timeslot.getTimeslot() == 0)
{
mESSProcessor0.process((AbstractVoiceTimeslot)timeslot);
@@ -139,8 +158,6 @@ public class P25P2MessageProcessor implements Listener<IMessage>
}
}
mMessageListener.receive(timeslot);
}
else
{

View File

@@ -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
@@ -78,9 +78,21 @@ public class L3HarrisTalkerAlias extends MacStructure
{
if(mAliasIdentifier == null)
{
int length = getLength() * 8;
String alias = new String(getMessage().getSubMessage(ALIAS_START + getOffset(), length + getOffset()).getBytes()).trim();
mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
int length = getLength();
if(length > 0)
{
length *= 8;
length += getOffset();
if(length > getMessage().size())
{
length = getMessage().size();
}
String alias = new String(getMessage().getSubMessage(ALIAS_START + getOffset(), length).getBytes()).trim();
mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
}
}
return mAliasIdentifier;

View File

@@ -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.module.decode.p25.phase2.timeslot;
@@ -25,7 +22,6 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import java.util.ArrayList;
import java.util.List;
@@ -61,7 +57,7 @@ public class Voice2Timeslot extends AbstractVoiceTimeslot
/**
* Voice frames contained in this timeslot
*/
public List<BinaryMessage> getVoiceFrames()
public synchronized List<BinaryMessage> getVoiceFrames()
{
if(mVoiceFrames == null)
{

View File

@@ -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.module.decode.p25.phase2.timeslot;
@@ -25,7 +22,6 @@ package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import java.util.ArrayList;
import java.util.List;
@@ -62,7 +58,7 @@ public class Voice4Timeslot extends AbstractVoiceTimeslot
/**
* Voice frames contained in this timeslot
*/
public List<BinaryMessage> getVoiceFrames()
public synchronized List<BinaryMessage> getVoiceFrames()
{
if(mVoiceFrames == null)
{
@@ -71,7 +67,6 @@ public class Voice4Timeslot extends AbstractVoiceTimeslot
mVoiceFrames.add(getMessage().getSubMessage(FRAME_2_START, FRAME_2_START + FRAME_LENGTH));
mVoiceFrames.add(getMessage().getSubMessage(FRAME_3_START, FRAME_3_START + FRAME_LENGTH));
mVoiceFrames.add(getMessage().getSubMessage(FRAME_4_START, FRAME_4_START + FRAME_LENGTH));
return mVoiceFrames;
}
return mVoiceFrames;

View File

@@ -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
@@ -327,22 +327,7 @@ public class PassportDecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
protected void resetState()
{

View File

@@ -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
@@ -58,22 +58,7 @@ public class Tait1200DecoderState extends DecoderState
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void init()
{
}
public void init() {}
@Override
public void receive(IMessage message)

View File

@@ -0,0 +1,325 @@
/*
* *****************************************************************************
* 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.monitor;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JOptionPane;
/**
* Utility class for monitoring system components and producing logging reports.
*/
public class DiagnosticMonitor
{
private static final Logger LOGGER = LoggerFactory.getLogger(DiagnosticMonitor.class);
private final LoggingSuppressor LOG_SUPPRESSOR = new LoggingSuppressor(LOGGER);
private static final String DIVIDER = "\n\n=========================================================================\n\n";
private UserPreferences mUserPreferences;
private ChannelProcessingManager mChannelProcessingManager;
private TunerManager mTunerManager;
private ScheduledFuture<?> mBlockedThreadMonitorHandle;
private BlockedThreadMonitor mMonitor = new BlockedThreadMonitor();
private boolean mUserAlertedToBlockedThreadCondition = false;
private Map<Integer,Integer> mBlockedThreadDetectionCountMap = new HashMap<>();
private boolean mHeadless;
/**
* Constructs an instance
* @param userPreferences for application logging directory lookup.
* @param channelProcessingManager for accessing running channel information
* @param tunerManager for accessing allocated tuner channel information
* @param headless to indicate if the thread deadlock monitor should show a user notification.
*/
public DiagnosticMonitor(UserPreferences userPreferences, ChannelProcessingManager channelProcessingManager,
TunerManager tunerManager, boolean headless)
{
mUserPreferences = userPreferences;
mChannelProcessingManager = channelProcessingManager;
mTunerManager = tunerManager;
mHeadless = headless;
}
/**
* Starts monitoring for blocked threads
*/
public void start()
{
if(mBlockedThreadMonitorHandle != null)
{
mBlockedThreadMonitorHandle.cancel(true);
}
if(mUserPreferences.getApplicationPreference().isAutomaticDiagnosticMonitoring())
{
LOGGER.info("Diagnostic monitoring enabled running every 30 seconds");
mBlockedThreadMonitorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mMonitor, 30, 30, TimeUnit.SECONDS);
}
else
{
LOGGER.info("Diagnostic monitoring disabled per user preference (application).");
}
}
/**
* Stops monitoring for blocked threads.
*/
public void stop()
{
if(mBlockedThreadMonitorHandle != null)
{
mBlockedThreadMonitorHandle.cancel(true);
}
mBlockedThreadMonitorHandle = null;
}
/**
* Checks for blocked threads and on discovery, generates a diagnostic report and notifies the user (once).
*/
private void checkForBlockedThreads()
{
if(!mUserAlertedToBlockedThreadCondition)
{
try
{
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long ids[] = bean.findDeadlockedThreads();
if(ids != null)
{
mUserAlertedToBlockedThreadCondition = true;
ThreadInfo threadInfo[] = bean.getThreadInfo(ids);
StringBuilder sb = new StringBuilder();
sb.append("sdrtrunk detected a critical application error with a threading deadlock, described as follows:\n");
for (ThreadInfo threadInfo1 : threadInfo)
{
sb.append("Thread ID[").append(threadInfo1.getThreadId());
sb.append("] Name [").append(threadInfo1.getThreadName());
sb.append("] Lock [").append(threadInfo1.getLockName());
sb.append("] Owned By [ID:").append(threadInfo1.getLockOwnerId());
sb.append(" | NAME:").append(threadInfo1.getLockName());
sb.append("]\n");
}
LOGGER.error(sb.toString());
Path reportPath = generateProcessingDiagnosticReport(sb + DIVIDER);
LOGGER.error("Thread deadlock report generated: " + reportPath);
if(!mHeadless)
{
String title = "sdrtrunk: Critical Error Detected";
String message = "The sdrtrunk application has detected a thread deadlock situation.\n" +
"The application may degrade over time and eventually run out of memory.\n" +
"A diagnostic report was generated. Please open an issue on the GitHub\n" +
"website and attach this diagnostic report:\n\n" + reportPath.toString();
JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE);
}
}
}
catch(Exception e)
{
LOG_SUPPRESSOR.error("run error", 1, "Error while monitoring for deadlocked " +
"threads: " + e.getLocalizedMessage());
//Set the flag so that we don't try to run again.
mUserAlertedToBlockedThreadCondition = true;
}
}
}
/**
* Creates a diagnostic report containing state information for channels that are in a processing state.
* @param message to prepend to the report
* @return path for the log file that was created.
*/
public Path generateProcessingDiagnosticReport(String message) throws IOException
{
StringBuilder sb = new StringBuilder();
sb.append(message);
sb.append("\n\nsdrtrunk Processing Diagnostic Report\n");
sb.append(DIVIDER);
sb.append(getEnvironmentReport());
sb.append(DIVIDER);
sb.append(mTunerManager.getDiscoveredTunerModel().getDiagnosticReport());
sb.append(DIVIDER);
sb.append(mChannelProcessingManager.getDiagnosticInformation());
sb.append(DIVIDER);
sb.append(mChannelProcessingManager.getChannelMetadataModel().getDiagnosticInformation());
sb.append(DIVIDER);
sb.append(getThreadDumpReport());
sb.append(DIVIDER);
Path logDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog();
String file = TimeStamp.getFileFormattedDateTime() + "_sdrtrunk_processing_diagnostic_report.log";
Path output = logDirectory.resolve(file);
Files.write(output, sb.toString().getBytes());
return output;
}
/**
* Dumps the current threads to a log file with current date and time to the application log directory.
* @return path to the thread dump log file that was created.
* @throws IOException if there is an issue writing the contents to the log file.
*/
public Path generateThreadDumpReport() throws IOException
{
String report = getThreadDumpReport();
Path logDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog();
String file = TimeStamp.getFileFormattedDateTime() + "_sdrtrunk_thread_dump.log";
Path output = logDirectory.resolve(file);
Files.write(output, report.getBytes());
return output;
}
/**
* Creates a thread dump report.
* @return report text.
*/
public String getThreadDumpReport()
{
StringBuilder sb = new StringBuilder();
sb.append("Thread Dump Report\n\n");
for(Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet())
{
sb.append(entry.getKey() + " " + entry.getKey().getState()).append("\n");
for(StackTraceElement ste : entry.getValue())
{
sb.append("\tat " + ste).append("\n");
}
sb.append("\n\n");
}
return sb.toString();
}
/**
* Generates a JVM and application environment report
*/
public String getEnvironmentReport()
{
StringBuilder sb = new StringBuilder();
sb.append("JVM and Application Environment Report\n");
Attributes atts = findManifestAttributes();
if (atts != null) {
sb.append("\nVersion : " + atts.getValue("Implementation-Version"));
sb.append("\nGradle Version : " + atts.getValue("Created-By"));
sb.append("\nBuild Timestamp : " + atts.getValue("Build-Timestamp"));
sb.append("\nBuild-JDK : " + atts.getValue("Build-JDK"));
sb.append("\nBuild OS : " + atts.getValue("Build-OS"));
}
else
{
sb.append("\nApplication: no build information available");
}
sb.append("\nHost OS Name: " + System.getProperty("os.name"));
sb.append("\nHost OS Arch: " + System.getProperty("os.arch"));
sb.append("\nHost OS Version: " + System.getProperty("os.version"));
sb.append("\nHost CPU Cores: " + Runtime.getRuntime().availableProcessors());
sb.append("\nHost Max Java Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory()));
sb.append("\nHost Allocated Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().totalMemory()));
sb.append("\nHost Free Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().freeMemory()));
sb.append("\nHost Used Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory()));
sb.append("\nStorage Directories:");
sb.append("\n Application Root: " + mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot());
sb.append("\n Application Log: " + mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog());
sb.append("\n Event Log: " + mUserPreferences.getDirectoryPreference().getDirectoryEventLog());
sb.append("\n Playlist: " + mUserPreferences.getDirectoryPreference().getDirectoryPlaylist());
sb.append("\n Recordings: " + mUserPreferences.getDirectoryPreference().getDirectoryRecording());
sb.append("\n");
return sb.toString();
}
/**
* 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()) {
try {
Manifest manifest = new Manifest(resources.nextElement().openStream());
Attributes atts = manifest.getMainAttributes();
Boolean hasTitle = atts.containsValue("sdrtrunk project");
if (hasTitle) {
return atts;
}
} catch (IOException E) {
return null;
}
}
}
catch (Exception ex)
{
return null;
}
return null;
}
/**
* Runnable to periodically check for blocked threads
*/
public class BlockedThreadMonitor implements Runnable
{
@Override
public void run()
{
try
{
checkForBlockedThreads();
}
catch(Throwable t)
{
LOG_SUPPRESSOR.error("Error", 3, "Error while checking for blocked threads", t);
}
}
}
}

View File

@@ -24,7 +24,7 @@ import io.github.dsheirer.preference.application.ApplicationPreference;
import io.github.dsheirer.preference.calibration.VectorCalibrationPreference;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.preference.directory.DirectoryPreference;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.preference.duplicate.CallManagementPreference;
import io.github.dsheirer.preference.event.DecodeEventPreference;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
import io.github.dsheirer.preference.javafx.JavaFxPreferences;
@@ -60,7 +60,7 @@ public class UserPreferences implements Listener<PreferenceType>
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DecodeEventPreference mDecodeEventPreference;
private DirectoryPreference mDirectoryPreference;
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private CallManagementPreference mDuplicateCallDetectionPreference;
private JmbeLibraryPreference mJmbeLibraryPreference;
private MP3Preference mMP3Preference;
private PlaybackPreference mPlaybackPreference;
@@ -208,7 +208,7 @@ public class UserPreferences implements Listener<PreferenceType>
/**
* Duplicate call detection preferences
*/
public DuplicateCallDetectionPreference getDuplicateCallDetectionPreference()
public CallManagementPreference getDuplicateCallDetectionPreference()
{
return mDuplicateCallDetectionPreference;
}
@@ -222,7 +222,7 @@ public class UserPreferences implements Listener<PreferenceType>
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive);
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
mDuplicateCallDetectionPreference = new DuplicateCallDetectionPreference(this::receive);
mDuplicateCallDetectionPreference = new CallManagementPreference(this::receive);
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
mMP3Preference = new MP3Preference(this::receive);
mPlaybackPreference = new PlaybackPreference(this::receive);

View File

@@ -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
@@ -31,11 +31,13 @@ import org.slf4j.LoggerFactory;
*/
public class ApplicationPreference extends Preference
{
private static final String PREFERENCE_KEY_CHANNEL_AUTO_DIAGNOSTIC_MONITORING = "automatic.diagnostic.monitoring";
private static final String PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT = "channel.auto.start.timeout";
private final static Logger mLog = LoggerFactory.getLogger(ApplicationPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(ApplicationPreference.class);
private Integer mChannelAutoStartTimeout;
private Boolean mAutomaticDiagnosticMonitoring;
/**
* Constructs an instance
@@ -77,4 +79,29 @@ public class ApplicationPreference extends Preference
mPreferences.putInt(PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT, timeout);
notifyPreferenceUpdated();
}
/**
* Indicates if automatic diagnostic monitoring is enabled.
* @return enabled.
*/
public boolean isAutomaticDiagnosticMonitoring()
{
if(mAutomaticDiagnosticMonitoring == null)
{
mAutomaticDiagnosticMonitoring = mPreferences.getBoolean(PREFERENCE_KEY_CHANNEL_AUTO_DIAGNOSTIC_MONITORING, true);
}
return mAutomaticDiagnosticMonitoring;
}
/**
* Sets the enabled state for automatic diagnostic monitoring.
* @param enabled true to turn on monitoring.
*/
public void setAutomaticDiagnosticMonitoring(boolean enabled)
{
mAutomaticDiagnosticMonitoring = enabled;
mPreferences.putBoolean(PREFERENCE_KEY_CHANNEL_AUTO_DIAGNOSTIC_MONITORING, enabled);
notifyPreferenceUpdated();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,38 +19,40 @@
package io.github.dsheirer.preference.duplicate;
import io.github.dsheirer.audio.broadcast.PatchGroupStreamingOption;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import java.util.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.prefs.Preferences;
/**
* User preferences for duplicate call detection
* User preferences for call management
*/
public class DuplicateCallDetectionPreference extends Preference
public class CallManagementPreference extends Preference
{
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP = "duplicate.call.detect.talkgroup";
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_RADIO = "duplicate.call.detect.radio";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK = "suppress.duplicate.audio.playback";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING = "suppress.duplicate.audio.recording";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING = "suppress.duplicate.audio.streaming";
private static final String PREFERENCE_KEY_PATCHGROUP_STREAMING = "patchgroup.streaming";
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetectionPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(DuplicateCallDetectionPreference.class);
private final static Logger mLog = LoggerFactory.getLogger(CallManagementPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(CallManagementPreference.class);
private Boolean mDuplicateCallDetectionByTalkgroupEnabled;
private Boolean mDuplicateCallDetectionByRadioEnabled;
private Boolean mDuplicatePlaybackSuppressionEnabled;
private Boolean mDuplicateRecordingSuppressionEnabled;
private Boolean mDuplicateStreamingSuppressionEnabled;
private PatchGroupStreamingOption mPatchGroupStreamingOption;
/**
* Constructs an instance
* @param updateListener to receive notifications that a preference has been updated
*/
public DuplicateCallDetectionPreference(Listener<PreferenceType> updateListener)
public CallManagementPreference(Listener<PreferenceType> updateListener)
{
super(updateListener);
}
@@ -182,6 +184,51 @@ public class DuplicateCallDetectionPreference extends Preference
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING, enabled);
mDuplicateStreamingSuppressionEnabled = enabled;
notifyPreferenceUpdated();;
notifyPreferenceUpdated();
}
/**
* Patch group streaming option preference.
* @return option.
*/
public PatchGroupStreamingOption getPatchGroupStreamingOption()
{
if(mPatchGroupStreamingOption == null)
{
String option = mPreferences.get(PREFERENCE_KEY_PATCHGROUP_STREAMING, PatchGroupStreamingOption.PATCH_GROUP.name());
if(option != null)
{
try
{
mPatchGroupStreamingOption = PatchGroupStreamingOption.valueOf(option);
}
catch(Exception e)
{
//Ignore ... we'll use the default value.
}
}
if(mPatchGroupStreamingOption == null)
{
mPatchGroupStreamingOption = PatchGroupStreamingOption.PATCH_GROUP;
}
}
return mPatchGroupStreamingOption;
}
/**
* Sets the patch group streaming option preference.
* @param patchGroupStreamingOption preference
*/
public void setPatchGroupStreamingOption(PatchGroupStreamingOption patchGroupStreamingOption)
{
if(patchGroupStreamingOption != null)
{
mPreferences.put(PREFERENCE_KEY_PATCHGROUP_STREAMING, patchGroupStreamingOption.name());
mPatchGroupStreamingOption = patchGroupStreamingOption;
notifyPreferenceUpdated();
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.wave.AudioMetadata;
import io.github.dsheirer.record.wave.AudioMetadataUtils;
@@ -58,14 +59,28 @@ public class AudioSegmentRecorder
*/
public static void record(AudioSegment audioSegment, Path path, RecordFormat recordFormat,
UserPreferences userPreferences) throws IOException
{
record(audioSegment, path, recordFormat, userPreferences, audioSegment.getIdentifierCollection());
}
/**
* Records the audio segment to the specified path using the specified recording format
* @param audioSegment to record
* @param path for the recording
* @param recordFormat to use (WAVE, MP3)
* @param identifierCollection to use instead of the audioSegment's embedded collection.
* @throws IOException on any errors
*/
public static void record(AudioSegment audioSegment, Path path, RecordFormat recordFormat,
UserPreferences userPreferences, IdentifierCollection identifierCollection) throws IOException
{
switch(recordFormat)
{
case MP3:
recordMP3(audioSegment, path, userPreferences);
recordMP3(audioSegment, path, userPreferences, identifierCollection);
break;
case WAVE:
recordWAVE(audioSegment, path);
recordWAVE(audioSegment, path, identifierCollection);
break;
default:
throw new IllegalArgumentException("Unrecognized recording format [" + recordFormat.name() + "]");
@@ -76,16 +91,19 @@ public class AudioSegmentRecorder
* Records the audio segment as an MP3 file to the specified path.
* @param audioSegment to record
* @param path for the recording
* @param userPreferences for configuration
* @param identifierCollection to use instead of the collection embedded in the audio segment
* @throws IOException on any errors
*/
public static void recordMP3(AudioSegment audioSegment, Path path, UserPreferences userPreferences) throws IOException
public static void recordMP3(AudioSegment audioSegment, Path path, UserPreferences userPreferences,
IdentifierCollection identifierCollection) throws IOException
{
if(audioSegment.hasAudio())
{
OutputStream outputStream = new FileOutputStream(path.toFile());
//Write ID3 metadata
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(audioSegment.getIdentifierCollection(),
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(identifierCollection,
audioSegment.getAliasList());
byte[] id3Bytes = AudioMetadataUtils.getMP3ID3(metadataMap);
@@ -123,9 +141,10 @@ public class AudioSegmentRecorder
* Records the audio segment as a WAVe file to the specified path.
* @param audioSegment to record
* @param path for the recording
* @param identifierCollection to use instead of the audioSegment's embedded identifier collection
* @throws IOException on any errors
*/
public static void recordWAVE(AudioSegment audioSegment, Path path) throws IOException
public static void recordWAVE(AudioSegment audioSegment, Path path, IdentifierCollection identifierCollection) throws IOException
{
if(audioSegment.hasAudio())
{
@@ -136,7 +155,7 @@ public class AudioSegmentRecorder
writer.writeData(ConversionUtils.convertToSigned16BitSamples(audioBuffer));
}
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(audioSegment.getIdentifierCollection(),
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(identifierCollection,
audioSegment.getAliasList());
ByteBuffer listChunk = AudioMetadataUtils.getLISTChunk(metadataMap);

View File

@@ -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
@@ -273,19 +273,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
*/
public long getFrequency()
{
long frequency;
try
{
getFrequencyControllerLock().lock();
frequency = mFrequencyController.getFrequency();
}
finally
{
getFrequencyControllerLock().unlock();
}
return frequency;
return mFrequencyController.getFrequency();
}
@Override

View File

@@ -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
@@ -72,6 +72,7 @@ import io.github.dsheirer.source.tuner.sdrplay.api.device.Device;
import io.github.dsheirer.source.tuner.sdrplay.api.device.DeviceInfo;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Rsp1Device;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Rsp1aDevice;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Rsp1bDevice;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Rsp2Device;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDuoDevice;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDxDevice;
@@ -87,6 +88,12 @@ import io.github.dsheirer.source.tuner.sdrplay.rsp1a.IControlRsp1a;
import io.github.dsheirer.source.tuner.sdrplay.rsp1a.Rsp1aTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp1a.Rsp1aTunerController;
import io.github.dsheirer.source.tuner.sdrplay.rsp1a.Rsp1aTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.ControlRsp1b;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.DiscoveredRsp1bTuner;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.IControlRsp1b;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.Rsp1bTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.Rsp1bTunerController;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.Rsp1bTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.rsp2.ControlRsp2;
import io.github.dsheirer.source.tuner.sdrplay.rsp2.DiscoveredRsp2Tuner;
import io.github.dsheirer.source.tuner.sdrplay.rsp2.IControlRsp2;
@@ -148,6 +155,9 @@ public class TunerFactory
case RSP1A:
tuners.add(new DiscoveredRsp1aTuner(deviceInfo, channelizerType));
break;
case RSP1B:
tuners.add(new DiscoveredRsp1bTuner(deviceInfo, channelizerType));
break;
case RSP2:
tuners.add(new DiscoveredRsp2Tuner(deviceInfo, channelizerType));
break;
@@ -228,6 +238,14 @@ public class TunerFactory
return new RspTuner(rsp1aTunerController, tunerErrorListener, channelizerType);
}
break;
case RSP1B:
if(device instanceof Rsp1bDevice rsp1bDevice)
{
IControlRsp1b controlRsp1b = new ControlRsp1b(rsp1bDevice);
Rsp1bTunerController rsp1bTunerController = new Rsp1bTunerController(controlRsp1b, tunerErrorListener);
return new RspTuner(rsp1bTunerController, tunerErrorListener, channelizerType);
}
break;
case RSP2:
if(device instanceof Rsp2Device rsp2Device)
{
@@ -411,6 +429,8 @@ public class TunerFactory
return new Rsp1TunerConfiguration(uniqueID);
case RSP_1A:
return new Rsp1aTunerConfiguration(uniqueID);
case RSP_1B:
return new Rsp1bTunerConfiguration(uniqueID);
case RSP_2:
return new Rsp2TunerConfiguration(uniqueID);
case RSP_DUO_1:
@@ -451,6 +471,8 @@ public class TunerFactory
return new Rsp1TunerEditor(userPreferences, tunerManager, discoveredRspTuner);
case RSP1A:
return new Rsp1aTunerEditor(userPreferences, tunerManager, discoveredRspTuner);
case RSP1B:
return new Rsp1bTunerEditor(userPreferences, tunerManager, discoveredRspTuner);
case RSP2:
return new Rsp2TunerEditor(userPreferences, tunerManager, discoveredRspTuner);
case RSPdx:

View File

@@ -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
@@ -40,6 +40,7 @@ public enum TunerType
RAFAELMICRO_R828D("R828D"),
RSP_1("RSP1"),
RSP_1A("RSP1A"),
RSP_1B("RSP1B"),
RSP_2("RSP2"),
RSP_DUO_1("RSPduo Tuner 1"),
RSP_DUO_2("RSPduo Tuner 2"),

View File

@@ -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
@@ -59,13 +59,27 @@ public abstract class DiscoveredTuner implements ITunerErrorListener
* Logs current state of the tuner
*/
public void logState()
{
mLog.info(getDiagnosticReport());
}
/**
* Generates a state report for this tuner.
* @return
*/
public String getDiagnosticReport()
{
StringBuilder sb = new StringBuilder();
sb.append("Discovered Tuner: ").append(getId());
sb.append("\n\tClass:").append(getClass());
if(hasTuner())
{
sb.append("\n\tTuner - Frequency:").append(getTuner().getTunerController().getFrequency());
sb.append("\n\tTuner Class:").append(getTuner().getClass());
sb.append("\n\tTuner Controller Class:").append(getTuner().getTunerController().getClass());
sb.append("\n\tFrequency:").append(getTuner().getTunerController().getFrequency());
sb.append("\n\tError:").append(getErrorMessage());
sb.append("\n\tChannel Manager Class:").append(getTuner().getChannelSourceManager().getClass());
sb.append("\n\tChannel Manager:").append(getTuner().getChannelSourceManager().getStateDescription());
}
else
@@ -73,7 +87,7 @@ public abstract class DiscoveredTuner implements ITunerErrorListener
sb.append("\n\tTuner - no tuner");
}
mLog.info(sb.toString());
return sb.toString();
}
/**

View File

@@ -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
@@ -72,6 +72,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
.append("] Frequency [").append(channelSource.getFrequency())
.append("] Mixer [").append(channelSource.getMixerFrequency())
.append("]");
sb.append(" HASH:").append(Integer.toHexString(channelSource.hashCode()).toUpperCase());
}
return sb.toString();

View File

@@ -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
@@ -776,16 +776,4 @@ public class TunerManager implements IDiscoveredTunerStatusListener
}
}
}
public static void main(String[] args)
{
mLog.info("Starting ...");
UserPreferences userPreferences = new UserPreferences();
TunerManager tunerManager = new TunerManager(userPreferences);
tunerManager.start();
while(true);
// mLog.info("Finished!");
}
}

View File

@@ -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,7 +28,6 @@ import io.github.dsheirer.source.tuner.sdrplay.api.callback.IStreamListener;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Device;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.IfMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.LoMode;
import io.github.dsheirer.util.ThreadPool;
@@ -45,10 +44,11 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
private static final Logger mLog = LoggerFactory.getLogger(ControlRsp.class);
private T mDevice;
protected RspSampleRate mSampleRate = RspSampleRate.RATE_8_000;
protected int mGain;
protected int mLNA;
protected int mBasebandGainReduction;
private IDeviceEventListener mDeviceEventListener;
private IStreamListener mStreamListener;
protected WeakReference<IGainOverloadListener> mGainOverloadReference;
protected WeakReference<ITunerStatusListener> mGainOverloadReference;
//Streaming control lock and boolean status indicator. Access to the boolean indicator is protected by the lock.
protected ReentrantLock mStreamingLock = new ReentrantLock();
@@ -242,21 +242,28 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
}
}
/**
* Sets the gain index
* @param gain index value (0 - 28)
* @throws SDRPlayException
*/
@Override
public void setGain(int gain) throws SDRPlayException
public void setGain(int lna, int gr) throws SDRPlayException
{
validateGain(gain);
int lnaLimited = Math.max(lna, 0);
lnaLimited = Math.min(lnaLimited, getMaximumLNASetting());
if(gain != mGain)
int grLimited = Math.max(gr, 20);
grLimited = Math.min(grLimited, 59);
if(lna != lnaLimited || gr != grLimited)
{
mGain = gain;
getDevice().getTuner().setGain(mGain);
mLog.warn("Setting gain - adjusted LNA from [" + lna + "] to [" + lnaLimited + "] and GR from [" + gr + "] to [" + grLimited + "]");
}
getDevice().getTuner().setGain(lnaLimited, grLimited);
mLNA = lnaLimited;
mBasebandGainReduction = grLimited;
}
@Override
public float getCurrentGain() throws SDRPlayException
{
return getDevice().getTuner().getGain().getGainValues().getCurrent();
}
/**
@@ -264,7 +271,7 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
* @param listener to register
*/
@Override
public void setGainOverloadListener(IGainOverloadListener listener)
public void setGainOverloadListener(ITunerStatusListener listener)
{
if(mGainOverloadReference != null && mGainOverloadReference.get() != null)
{
@@ -289,7 +296,7 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
//Notify an optional weakly referenced, registered listener that gain overload has been acknowledged.
if(mGainOverloadReference != null)
{
IGainOverloadListener listener = mGainOverloadReference.get();
ITunerStatusListener listener = mGainOverloadReference.get();
if(listener != null)
{
@@ -317,6 +324,17 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
if(hasDevice())
{
getDevice().getTuner().setFrequency(frequency);
//Notify an optional weakly referenced, registered listener that gain overload has been acknowledged.
if(mGainOverloadReference != null)
{
ITunerStatusListener listener = mGainOverloadReference.get();
if(listener != null)
{
ThreadPool.CACHED.submit(() -> listener.frequencyUpdated());
}
}
}
else
{
@@ -343,27 +361,23 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
}
/**
* Current gain index
* @return gain index
* Current LNA gain setting
* @return LNA gain
*/
@Override
public int getGain()
public int getLNA()
{
return mGain;
return mLNA;
}
/**
* Verifies that the requested gain index setting is within the range of valid gain values.
* @param gain to validate
* @throws SDRPlayException if the gain index value is outside the range of valid values.
* Current Baseband gain reduction setting
* @return baseband gain reduction
*/
protected void validateGain(int gain) throws SDRPlayException
@Override
public int getBasebandGainReduction()
{
if(gain < GainReduction.MIN_GAIN_INDEX || gain > GainReduction.MAX_GAIN_INDEX)
{
throw new SDRPlayException("Invalid gain index value [" + gain + "]. Valid range is " +
GainReduction.MIN_GAIN_INDEX + " - " + GainReduction.MAX_GAIN_INDEX);
}
return mBasebandGainReduction;
}
/**

View File

@@ -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
@@ -116,16 +116,36 @@ public interface IControlRsp
double getCurrentSampleRate();
/**
* Sets the gain index value.
* @param gain index value (0 - 28)
* Set the lna state and gain reduction dB values separately.
* @param lna state (varies by tuner and frequency range)
* @param basebandGainReduction setting (20-59)
* @throws SDRPlayException
*/
void setGain(int gain) throws SDRPlayException;
void setGain(int lna, int basebandGainReduction) throws SDRPlayException;
/**
* Current gain index value
* @return gain index value (0 - 28)
* Current gain setting as reported by the device.
* @return gain value in decibels
*/
int getGain();
float getCurrentGain() throws SDRPlayException;
/**
* Current LNA gain index value
* @return LNA gain value.
*/
int getLNA();
/**
* Maximum LNA setting index value.
* @return max LNA that varies by tuner type.
*/
int getMaximumLNASetting();
/**
* Current gain reduction value.
* @return gain reduction value.
*/
int getBasebandGainReduction();
/**
* Current IF AGC mode setting.
@@ -143,5 +163,5 @@ public interface IControlRsp
* Registers a listener to receive notifications of gain overload.
* @param listener to register
*/
void setGainOverloadListener(IGainOverloadListener listener);
void setGainOverloadListener(ITunerStatusListener listener);
}

View File

@@ -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,7 +24,16 @@ import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
/**
* Listener interface to receive notifications of gain overload in the RSP device.
*/
public interface IGainOverloadListener
public interface ITunerStatusListener
{
/**
* Notify that there is a gain overload event in the specified tuner
* @param tunerSelect to id the tuner
*/
void notifyGainOverload(TunerSelect tunerSelect);
/**
* Notify that the tuner's frequency has changed.
*/
void frequencyUpdated();
}

View File

@@ -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,9 +24,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.sdrplay.rsp1.Rsp1TunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp1a.Rsp1aTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp1b.Rsp1bTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp2.Rsp2TunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.RspDuoTuner1Configuration;
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.RspDuoTuner2Configuration;
@@ -39,6 +39,7 @@ import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerConfiguration;
@JsonSubTypes({
@JsonSubTypes.Type(value = Rsp1TunerConfiguration.class, name = "rsp1TunerConfiguration"),
@JsonSubTypes.Type(value = Rsp1aTunerConfiguration.class, name = "rsp1aTunerConfiguration"),
@JsonSubTypes.Type(value = Rsp1bTunerConfiguration.class, name = "rsp1bTunerConfiguration"),
@JsonSubTypes.Type(value = Rsp2TunerConfiguration.class, name = "rsp2TunerConfiguration"),
@JsonSubTypes.Type(value = RspDuoTuner1Configuration.class, name = "rspDuoTuner1Configuration"),
@JsonSubTypes.Type(value = RspDuoTuner2Configuration.class, name = "rspDuoTuner2Configuration"),
@@ -50,7 +51,8 @@ public abstract class RspTunerConfiguration extends TunerConfiguration
public static final RspSampleRate DEFAULT_DUAL_TUNER_SAMPLE_RATE = RspSampleRate.DUO_RATE_2_000;
private RspSampleRate mRspSampleRate = DEFAULT_SINGLE_TUNER_SAMPLE_RATE;
private int mGain = 24;
private int mLNA = 0;
private int mBasebandGainReduction = 50;
private AgcMode mAgcMode = AgcMode.ENABLE;
/**
@@ -87,25 +89,41 @@ public abstract class RspTunerConfiguration extends TunerConfiguration
}
/**
* Gain index to use
* @return gain index, 0 - 28
* LNA setting
* @return LNA setting
*/
@JacksonXmlProperty(isAttribute = true, localName = "gain")
public int getGain()
@JacksonXmlProperty(isAttribute = true, localName = "lna")
public int getLNA()
{
return mGain;
return mLNA;
}
/**
* Sets the gain index
* @param gain index, 0 - 28
* Sets the LNA state
* @param LNA state, 0 - xx, varies by tuner and frequency range.
*/
public void setGain(int gain)
public void setLNA(int lna)
{
if(GainReduction.MIN_GAIN_INDEX <= gain && gain <= GainReduction.MAX_GAIN_INDEX)
{
mGain = gain;
}
mLNA = lna;
}
/**
* Baseband gain reduction
* @return gain reduction in range 20-59 dB
*/
@JacksonXmlProperty(isAttribute = true, localName = "gr")
public int getBasebandGainReduction()
{
return mBasebandGainReduction;
}
/**
* Sets the baseband gain reduction value.
* @param basebandGainReduction value
*/
public void setBasebandGainReduction(int basebandGainReduction)
{
mBasebandGainReduction = basebandGainReduction;
}
/**

View File

@@ -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
@@ -91,11 +91,13 @@ public abstract class RspTunerController<I extends IControlRsp> extends TunerCon
try
{
getControlRsp().setGain(rtc.getGain());
int lna = rtc.getLNA();
int gr = rtc.getBasebandGainReduction();
getControlRsp().setGain(lna, gr);
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP gain index to " + rtc.getGain());
mLog.error("Error setting RSP LNA [" + rtc.getLNA() + "] and baseband [" + rtc.getBasebandGainReduction() + "]");
}
}
}

View File

@@ -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,32 +24,37 @@ import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import io.github.dsheirer.util.ThreadPool;
import java.awt.Color;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
/**
* Abstract RSP tuner editor
*/
public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends TunerEditor<RspTuner,C> implements IGainOverloadListener
public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends TunerEditor<RspTuner,C> implements ITunerStatusListener
{
private Logger mLog = LoggerFactory.getLogger(RspTunerEditor.class);
private JSlider mGainSlider;
private JLabel mGainValueLabel;
protected static final String MANUAL = "Manual";
protected static final String AUTOMATIC = "Automatic";
private JToggleButton mAgcButton;
private JLabel mGainValueLabel;
private LnaSlider mLNASlider;
private IfGainSlider mIfGainSlider;
private JButton mGainOverloadButton;
private JPanel mGainPanel;
private AtomicBoolean mGainOverloadAlert = new AtomicBoolean();
/**
@@ -69,40 +74,23 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
}
/**
* Gain reduction slider control
* Gain controls panel
* @return gain panel
*/
protected JSlider getGainSlider()
protected JPanel getGainPanel()
{
if(mGainSlider == null)
if(mGainPanel == null)
{
mGainSlider = new JSlider(JSlider.HORIZONTAL, GainReduction.MIN_GAIN_INDEX,
GainReduction.MAX_GAIN_INDEX, GainReduction.MIN_GAIN_INDEX);
mGainSlider.setEnabled(false);
mGainSlider.setMajorTickSpacing(1);
mGainSlider.setPaintTicks(true);
mGainSlider.addChangeListener(event ->
{
int gain = mGainSlider.getValue();
if(hasTuner() && !isLoading())
{
try
{
getTunerController().getControlRsp().setGain(gain);
save();
}
catch(Exception e)
{
mLog.error("Couldn't set RSP gain to:" + gain, e);
JOptionPane.showMessageDialog(mGainSlider, "Couldn't set RSP gain value to " + gain);
}
}
getGainValueLabel().setText(String.valueOf(gain));
});
mGainPanel = new JPanel();
mGainPanel.setLayout(new MigLayout("insets 0","[][][grow,fill][][]",""));
mGainPanel.add(getGainValueLabel());
mGainPanel.add(getGainOverloadButton());
mGainPanel.add(new JLabel()); //empty label to grow to fill space
mGainPanel.add(new JLabel("IF Gain Mode:"));
mGainPanel.add(getAgcButton());
}
return mGainSlider;
return mGainPanel;
}
/**
@@ -126,7 +114,7 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
{
if(mAgcButton == null)
{
mAgcButton = new JToggleButton("IF AGC");
mAgcButton = new JToggleButton(MANUAL);
mAgcButton.setEnabled(false);
mAgcButton.addActionListener(e -> {
if(hasTuner() && !isLoading())
@@ -134,6 +122,7 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
try
{
getTunerController().getControlRsp().setAgcMode(mAgcButton.isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
updateGainLabel();
}
catch(SDRPlayException se)
{
@@ -141,6 +130,17 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
}
save();
}
getIfGainSlider().setEnabled(!getAgcButton().isSelected());
if(mAgcButton.isSelected())
{
getAgcButton().setText(AUTOMATIC);
}
else
{
getAgcButton().setText(MANUAL);
}
});
}
@@ -175,6 +175,12 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
}
}
@Override
public void frequencyUpdated()
{
updateLnaSlider();
}
/**
* Toggles the alert styling of the disabled gain overload button to indicate an alert condition or a normal
* operating condition.
@@ -194,9 +200,177 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
getGainOverloadButton().setForeground(getForeground());
getGainOverloadButton().setBackground(getBackground());
}
else
}
/**
* Updates the LNA slider with the number of LNA states available for the current frequency.
*/
protected void updateLnaSlider()
{
if(hasTuner())
{
mLog.info("Ignoring duplicate gain alerting - alert[" + alert + "] atomic [" + mGainOverloadAlert.get() + "]");
int max = getTunerController().getControlRsp().getMaximumLNASetting();
if(max != getLNASlider().getMaximum())
{
EventQueue.invokeLater(() -> {
setLoading(true);
//Adjust the value if it's less than max
getLNASlider().setValue(Math.min(getLNASlider().getValue(), max));
getLNASlider().setMaximum(max);
setLoading(false);
});
}
}
}
/**
* LNA Gain Slider
*/
protected LnaSlider getLNASlider()
{
if(mLNASlider == null)
{
mLNASlider = new LnaSlider();
mLNASlider.setEnabled(true);
mLNASlider.setMajorTickSpacing(1);
mLNASlider.setPaintTicks(true);
mLNASlider.addChangeListener(event ->
{
JSlider source = (JSlider)event.getSource();
if(!source.getValueIsAdjusting())
{
updateGain();
}
});
}
return mLNASlider;
}
/**
* Updates the gain settings when the LNA or Baseband gain value is changed by the user.
*/
private void updateGain()
{
int lna = getLNASlider().getLNA();
int gr = getIfGainSlider().getGR();
if(hasTuner() && !isLoading())
{
try
{
getTunerController().getControlRsp().setGain(lna, gr);
save();
updateGainLabel();
}
catch(Exception e)
{
mLog.error("Couldn't set RSP gain to LNA:" + lna + " Gain Reduction:" + gr, e);
JOptionPane.showMessageDialog(mIfGainSlider, "Couldn't set RSP gain value to LNA:" + lna + " Gain Reduction:" + gr);
}
}
}
protected void updateGainLabel()
{
try
{
float currentGain = getTunerController().getControlRsp().getCurrentGain();
getGainValueLabel().setText((int)currentGain + " dB");
}
catch(SDRPlayException se)
{
mLog.error("Error accessing current gain value from RSP tuner", se);
}
}
/**
* IF Gain Slider
*/
protected IfGainSlider getIfGainSlider()
{
if(mIfGainSlider == null)
{
mIfGainSlider = new IfGainSlider();
mIfGainSlider.setEnabled(true);
mIfGainSlider.setMajorTickSpacing(1);
mIfGainSlider.setPaintTicks(true);
mIfGainSlider.addChangeListener(event ->
{
JSlider source = (JSlider)event.getSource();
if(!source.getValueIsAdjusting())
{
updateGain();
}
});
}
return mIfGainSlider;
}
/**
* JSlider implementation that inverts the scale to support LNA values.
*/
public class LnaSlider extends JSlider
{
/**
* Constructs an instance
*/
public LnaSlider()
{
super(JSlider.HORIZONTAL, 0,9,9);
}
/**
* Slider value converted to the LNA scale.
* @return lna value.
*/
public int getLNA()
{
return getMaximum() - getValue();
}
/**
* Sets the slider with the value converted from the provided LNA value.
* @param lna value to apply
*/
public void setLNA(int lna)
{
setValue(getMaximum() - lna);
}
}
/**
* JSlider implementation that inverts the scale to support IF Gain (aka Gain Reduction) values.
*/
public class IfGainSlider extends JSlider
{
/**
* Constructs an instance
*/
public IfGainSlider()
{
super(JSlider.HORIZONTAL, 0, 39, 30);
}
/**
* Slider value converted to the gain reduction scale.
* @return gain reduction value.
*/
public int getGR()
{
return getIfGainSlider().getMaximum() - getIfGainSlider().getValue() + 20;
}
/**
* Sets the slider with the value converted from the provided gain reduction value.
* @param gainReduction value to apply
*/
public void setGR(int gainReduction)
{
setValue(getMaximum() - (gainReduction - 20));
}
}
}

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