From 7eec82deee7b49ef6ee00c0c112c92747cf8c800 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Mon, 12 Oct 2020 04:57:59 -0400 Subject: [PATCH] #6 DMR Decoder (#947) * #6 DMR decoder feature - work in progress * #6 DMR Decoder. * #6 DMR Decoder. Code review updates. Co-authored-by: Zhenyu Mao Co-authored-by: Denny --- .idea/dictionaries/denny.xml | 2 + .idea/misc.xml | 2 +- .idea/modules.xml | 10 - build.gradle | 1 + .../alias/action/AliasActionManager.java | 6 - .../dsheirer/alias/id/radio/RadioFormat.java | 4 + .../alias/id/radio/RadioFormatter.java | 3 +- .../alias/id/talkgroup/TalkgroupFormat.java | 4 +- .../id/talkgroup/TalkgroupFormatter.java | 3 +- .../dsheirer/audio/AbstractAudioModule.java | 23 +- .../io/github/dsheirer/audio/AudioModule.java | 11 +- .../audio/codec/mbe/AmbeAudioModule.java | 4 +- .../audio/codec/mbe/ImbeAudioModule.java | 2 +- .../audio/codec/mbe/JmbeAudioModule.java | 20 +- .../codec/mbe/MBECallSequenceRecorder.java | 5 - .../audio/playback/AudioChannelPanel.java | 4 +- .../dsheirer/audio/playback/AudioOutput.java | 3 +- .../dsheirer/audio/playback/AudioPanel.java | 2 +- .../audio/playback/AudioPlaybackManager.java | 3 +- .../github/dsheirer/bits/BinaryMessage.java | 18 + .../dsheirer/bits/SoftSyncDetector.java | 4 + .../channel/details/ChannelDetailPanel.java | 8 +- .../channel/metadata/ChannelAndMetadata.java | 60 + .../metadata/ChannelMetadataModel.java | 55 +- .../metadata/ChannelMetadataPanel.java | 46 +- .../channel/state/AbstractChannelState.java | 26 +- .../state/AlwaysUnsquelchedDecoderState.java | 6 +- .../state/ChangeChannelTimeoutEvent.java | 2 +- .../dsheirer/channel/state/DecoderState.java | 8 - .../channel/state/DecoderStateEvent.java | 93 +- .../DecoderStateNotificationEventCache.java | 62 + .../channel/state/MultiChannelState.java | 154 +-- .../channel/state/SingleChannelState.java | 83 +- .../dsheirer/controller/ControllerPanel.java | 2 +- .../dsheirer/controller/channel/Channel.java | 2 + ...hannelConfigurationChangeNotification.java | 47 + .../channel/ChannelConversionRequest.java | 59 + .../controller/channel/ChannelGrantEvent.java | 64 - .../channel/ChannelProcessingManager.java | 364 ++++-- .../event/ChannelStartProcessingRequest.java | 228 ++++ .../channel/event/PreloadDataContent.java | 58 + .../dsheirer/dsp/filter/FilterFactory.java | 4 +- .../dsp/filter/design/FilterViewer.java | 65 +- .../filter/design/FilterViewerLauncher.java | 35 + .../io/github/dsheirer/dsp/symbol/Dibit.java | 20 +- .../dsheirer/dsp/symbol/DibitDelayBuffer.java | 260 ++++ .../dsheirer/dsp/symbol/QPSKCarrierLock.java | 120 ++ .../io/github/dsheirer/edac/BCH_63_16_11.java | 178 +-- .../io/github/dsheirer/edac/BPTC_17_12_3.java | 62 + .../io/github/dsheirer/edac/BPTC_196_96.java | 284 +++++ .../github/dsheirer/edac/BerlekempMassey.java | 559 ++++++++ .../dsheirer/edac/BerlekempMassey_63.java | 521 -------- .../java/io/github/dsheirer/edac/CRCDMR.java | 290 +++++ .../java/io/github/dsheirer/edac/CRCUtil.java | 408 +++--- .../io/github/dsheirer/edac/Hamming13.java | 129 ++ .../io/github/dsheirer/edac/Hamming15.java | 156 +-- .../io/github/dsheirer/edac/Hamming16.java | 120 ++ .../io/github/dsheirer/edac/Hamming17.java | 123 ++ .../dsheirer/edac/ReedSolomon_12_9_4_DMR.java | 109 ++ ..._17.java => ReedSolomon_24_12_13_P25.java} | 19 +- .../edac/ReedSolomon_24_16_9_P25.java | 37 + .../dsheirer/edac/ReedSolomon_255_DMR.java | 44 + ..._29.java => ReedSolomon_44_16_29_P25.java} | 20 +- ..._29.java => ReedSolomon_63_35_29_P25.java} | 15 +- .../edac/ReedSolomon_63_47_17_P25.java | 32 + .../dsheirer/edac/ReedSolomon_63_P25.java | 44 + .../dsheirer/edac/trellis/DMR_3_4_Node.java | 6 + .../io/github/dsheirer/edac/trellis/Path.java | 8 + .../edac/trellis/ViterbiDecoder_3_4_DMR.java | 70 +- .../github/dsheirer/eventbus/MyEventBus.java | 7 +- .../io/github/dsheirer/filter/FilterSet.java | 5 + .../dsheirer/gui/JavaFxWindowManager.java | 4 +- .../java/io/github/dsheirer/gui/SDRTrunk.java | 6 +- .../gui/instrument/decoder/DMRPane.java | 201 +++ .../decoder/DecoderPaneFactory.java | 5 +- .../dsheirer/gui/playlist/PlaylistEditor.java | 4 +- .../gui/playlist/alias/AliasItemEditor.java | 15 +- .../alias/AliasViewByIdentifierEditor.java | 2 +- .../alias/identifier/RadioIdEditor.java | 4 + .../alias/identifier/TalkgroupEditor.java | 4 + .../channel/ChannelConfigurationEditor.java | 2 +- .../ChannelConfigurationEditorFactory.java | 2 + .../channel/DMRConfigurationEditor.java | 726 +++++++++++ .../channel/MPT1327ConfigurationEditor.java | 2 +- .../manager/PlaylistManagerEditor.java | 7 +- .../radioreference/FrequencyEditor.java | 2 +- .../radioreference/ModeDecoderType.java | 2 +- .../radioreference/RadioReferenceDecoder.java | 96 +- .../playlist/radioreference/SiteEditor.java | 21 +- .../SystemTalkgroupSelectionEditor.java | 7 +- .../radioreference/TalkgroupEditor.java | 4 +- .../gui/playlist/source/FrequencyEditor.java | 61 +- .../preference/PreferenceEditorFactory.java | 3 - .../gui/preference/PreferenceEditorType.java | 1 - .../gui/preference/UserPreferencesEditor.java | 3 +- .../decoder/JmbeLibraryPreferenceEditor.java | 9 +- .../directory/DirectoryPreferenceEditor.java | 7 +- ...nnelMultipleFrequencyPreferenceEditor.java | 123 -- .../io/github/dsheirer/identifier/Form.java | 6 + .../identifier/IdentifierCollection.java | 3 +- .../MutableIdentifierCollection.java | 77 +- .../decoder/ChannelStateIdentifier.java | 52 +- .../location/LocationIdentifier.java | 41 + .../dsheirer/identifier/location/Point.java | 59 + .../site}/SiteIdentifier.java | 19 +- .../io/github/dsheirer/jmbe/JmbeEditor.java | 2 +- .../github/dsheirer/log/ApplicationLog.java | 4 +- .../dsheirer/message/MessageHistory.java | 72 ++ .../message/MessageHistoryPreloadData.java | 39 + .../message/MessageHistoryRequest.java | 29 + .../message/MessageHistoryResponse.java | 49 + .../message/MessageProviderModule.java | 6 - .../dsheirer/message/SyncLossMessage.java | 32 +- .../github/dsheirer/module/HistoryModule.java | 101 ++ .../io/github/dsheirer/module/Module.java | 99 +- .../module/ModuleEventBusMessage.java | 28 + .../dsheirer/module/ProcessingChain.java | 116 +- .../dsheirer/module/decode/Decoder.java | 7 - .../module/decode/DecoderFactory.java | 104 +- .../dsheirer/module/decode/DecoderType.java | 13 +- .../module/decode/PrimaryDecoder.java | 9 - .../decode/config/DecodeConfiguration.java | 10 +- .../module/decode/dmr/DMRBurstFramer.java | 520 ++++++++ .../module/decode/dmr/DMRDecoder.java | 344 +++++ .../decode/dmr/DMRDecoderInstrumented.java | 123 ++ .../module/decode/dmr/DMRDecoderState.java | 1135 +++++++++++++++++ .../module/decode/dmr/DMRMessageFramer.java | 429 +++++++ .../decode/dmr/DMRMessageProcessor.java | 218 ++++ .../dmr/DMRNetworkConfigurationMonitor.java | 369 ++++++ .../DMRNetworkConfigurationPreloadData.java | 42 + .../module/decode/dmr/DMRSyncDetector.java | 178 +++ .../module/decode/dmr/DMRSyncPattern.java | 154 +++ .../decode/dmr/DMRTrafficChannelManager.java | 696 ++++++++++ .../module/decode/dmr/DecodeConfigDMR.java | 151 +++ .../decode/dmr/IDMRBurstDetectListener.java | 44 + .../decode/dmr/TimeslotAlignmentTracker.java | 155 +++ .../dmr/channel/DMRAbsoluteChannel.java | 80 ++ .../module/decode/dmr/channel/DMRChannel.java | 141 ++ .../decode/dmr/channel/DMRLogicalChannel.java | 94 ++ .../channel/ITimeslotFrequencyReceiver.java | 36 + .../decode/dmr/channel/TimeslotFrequency.java | 174 +++ .../dmr/event/DMRChannelGrantEvent.java | 178 +++ .../decode/dmr/event/DMRDecodeEvent.java | 52 + .../decode/dmr/identifier/DMRLocation.java | 60 + .../decode/dmr/identifier/DMRNetwork.java | 52 + .../decode/dmr/identifier/DMRRadio.java | 68 + .../module/decode/dmr/identifier/DMRSite.java | 53 + .../decode/dmr/identifier/DMRTalkgroup.java | 62 + .../dmr/identifier/DMRToneIdentifier.java | 49 + .../decode/dmr/identifier/DMRUnitStatus.java | 40 + .../module/decode/dmr/message/CACH.java | 187 +++ .../module/decode/dmr/message/DMRBurst.java | 56 + .../module/decode/dmr/message/DMRMessage.java | 84 ++ .../decode/dmr/message/DMRMessageFactory.java | 86 ++ .../decode/dmr/message/UnknownDMRMessage.java | 71 ++ .../message/data/DMRDataMessageFactory.java | 262 ++++ .../decode/dmr/message/data/DataMessage.java | 58 + .../data/DataMessageWithLinkControl.java | 89 ++ .../decode/dmr/message/data/IDLEMessage.java | 62 + .../decode/dmr/message/data/SlotType.java | 122 ++ .../dmr/message/data/UnknownDataMessage.java | 75 ++ .../dmr/message/data/block/DataBlock.java | 84 ++ .../message/data/block/DataBlock1Rate.java | 83 ++ .../message/data/block/DataBlock1_2Rate.java | 83 ++ .../message/data/block/DataBlock3_4Rate.java | 84 ++ .../message/data/block/UnknownDataBlock.java | 65 + .../dmr/message/data/csbk/CSBKMessage.java | 166 +++ .../message/data/csbk/CSBKMessageFactory.java | 426 +++++++ .../decode/dmr/message/data/csbk/Opcode.java | 203 +++ .../message/data/csbk/UnknownCSBKMessage.java | 97 ++ .../hytera/HyteraAdjacentSiteInformation.java | 94 ++ .../data/csbk/hytera/HyteraAnnouncement.java | 156 +++ .../data/csbk/hytera/HyteraXPTPreamble.java | 188 +++ .../data/csbk/hytera/HyteraXPTSiteState.java | 276 ++++ .../data/csbk/motorola/CapacityMaxAloha.java | 196 +++ .../csbk/motorola/CapacityPlusCSBKO_60.java | 74 ++ ...acityPlusDataRevertWindowAnnouncement.java | 162 +++ .../CapacityPlusDataRevertWindowGrant.java | 119 ++ .../csbk/motorola/CapacityPlusNeighbors.java | 342 +++++ .../csbk/motorola/CapacityPlusPreamble.java | 159 +++ .../motorola/CapacityPlusSystemStatus.java | 158 +++ .../csbk/motorola/ConnectPlusCSBKO_16.java | 129 ++ .../motorola/ConnectPlusDataChannelGrant.java | 173 +++ ...nnectPlusDataRevertWindowAnnouncement.java | 137 ++ .../ConnectPlusDataRevertWindowGrant.java | 122 ++ .../motorola/ConnectPlusNeighborReport.java | 143 +++ .../motorola/ConnectPlusOTAAnnouncement.java | 163 +++ .../ConnectPlusRegistrationRequest.java | 129 ++ .../ConnectPlusRegistrationResponse.java | 129 ++ .../ConnectPlusTalkgroupAffiliation.java | 131 ++ .../ConnectPlusTerminateChannelGrant.java | 155 +++ .../motorola/ConnectPlusVoiceChannelUser.java | 189 +++ .../dmr/message/data/csbk/standard/Aloha.java | 196 +++ .../dmr/message/data/csbk/standard/Clear.java | 265 ++++ .../message/data/csbk/standard/MoveTSCC.java | 207 +++ .../message/data/csbk/standard/Preamble.java | 161 +++ .../message/data/csbk/standard/Protect.java | 145 +++ .../standard/acknowledge/Acknowledge.java | 184 +++ .../acknowledge/AcknowledgeStatus.java | 90 ++ .../acknowledge/RegistrationAccepted.java | 66 + .../message/data/csbk/standard/ahoy/Ahoy.java | 135 ++ .../ahoy/AuthenticateRegisterRadioCheck.java | 159 +++ .../data/csbk/standard/ahoy/CancelCall.java | 104 ++ .../csbk/standard/ahoy/ServiceRadioCheck.java | 129 ++ .../csbk/standard/ahoy/StunReviveKill.java | 152 +++ .../data/csbk/standard/ahoy/UnknownAhoy.java | 153 +++ .../announcement/AdjacentSiteInformation.java | 272 ++++ .../AnnounceChannelFrequency.java | 131 ++ .../announcement/AnnounceWithdrawTSCC.java | 256 ++++ .../standard/announcement/Announcement.java | 135 ++ .../announcement/CallTimerParameters.java | 273 ++++ .../csbk/standard/announcement/LocalTime.java | 122 ++ .../announcement/MassRegistration.java | 127 ++ .../standard/announcement/VoteNowAdvice.java | 272 ++++ .../BroadcastTalkgroupVoiceChannelGrant.java | 155 +++ .../csbk/standard/grant/ChannelGrant.java | 191 +++ .../grant/DuplexPrivateDataChannelGrant.java | 168 +++ .../grant/DuplexPrivateVoiceChannelGrant.java | 168 +++ .../grant/PrivateDataChannelGrant.java | 167 +++ .../grant/PrivateVoiceChannelGrant.java | 153 +++ .../grant/TalkgroupDataChannelGrant.java | 169 +++ .../grant/TalkgroupVoiceChannelGrant.java | 155 +++ .../data/header/ConfirmedDataHeader.java | 115 ++ .../dmr/message/data/header/DataHeader.java | 101 ++ .../data/header/DefinedShortDataHeader.java | 141 ++ .../message/data/header/HeaderMessage.java | 48 + .../dmr/message/data/header/MBCHeader.java | 115 ++ .../message/data/header/OctetDataHeader.java | 86 ++ .../data/header/PacketSequenceHeader.java | 127 ++ .../data/header/ProprietaryDataHeader.java | 111 ++ .../data/header/RawShortDataHeader.java | 101 ++ .../data/header/ResponseDataHeader.java | 168 +++ .../message/data/header/ShortDataHeader.java | 170 +++ .../message/data/header/StatusDataHeader.java | 208 +++ .../dmr/message/data/header/UDTHeader.java | 235 ++++ .../data/header/UnconfirmedDataHeader.java | 88 ++ .../dmr/message/data/header/VoiceHeader.java | 57 + .../hytera/HyteraProprietaryDataHeader.java | 78 ++ .../motorola/MNISProprietaryDataHeader.java | 167 +++ .../MotorolaProprietaryDataHeader.java | 97 ++ .../decode/dmr/message/data/lc/LCMessage.java | 76 ++ .../dmr/message/data/lc/LCMessageFactory.java | 174 +++ .../decode/dmr/message/data/lc/LCOpcode.java | 195 +++ .../lc/full/AbstractVoiceChannelUser.java | 58 + .../message/data/lc/full/FLCAssembler.java | 223 ++++ .../message/data/lc/full/FullLCMessage.java | 102 ++ .../message/data/lc/full/GPSInformation.java | 119 ++ .../data/lc/full/GroupVoiceChannelUser.java | 113 ++ .../message/data/lc/full/TerminatorData.java | 176 +++ .../lc/full/UnitToUnitVoiceChannelUser.java | 111 ++ .../data/lc/full/UnknownFullLCMessage.java | 90 ++ .../data/lc/full/hytera/HyteraFullLC.java | 143 +++ .../hytera/HyteraGroupVoiceChannelUser.java | 106 ++ .../data/lc/full/hytera/HyteraTerminator.java | 106 ++ .../HyteraUnitToUnitVoiceChannelUser.java | 106 ++ .../CapacityPlusGroupVoiceChannelUser.java | 205 +++ .../CapacityPlusWideAreaVoiceChannelUser.java | 210 +++ .../data/lc/shorty/ActivityUpdateMessage.java | 106 ++ .../lc/shorty/CapacityPlusRestChannel.java | 150 +++ .../lc/shorty/ConnectPlusControlChannel.java | 103 ++ .../lc/shorty/ConnectPlusTrafficChannel.java | 104 ++ .../ControlChannelSystemParameters.java | 112 ++ .../data/lc/shorty/HyteraXPTChannel.java | 123 ++ .../message/data/lc/shorty/NullMessage.java | 61 + .../message/data/lc/shorty/SLCAssembler.java | 248 ++++ .../data/lc/shorty/ShortLCMessage.java | 104 ++ .../data/lc/shorty/UnknownShortLCMessage.java | 58 + .../dmr/message/data/mbc/MBCAssembler.java | 134 ++ .../data/mbc/MBCContinuationBlock.java | 79 ++ .../dmr/message/data/mbc/MultiCSBK.java | 56 + .../message/data/mbc/UnknownMultiCSBK.java | 96 ++ .../message/data/packet/DMRPacketMessage.java | 99 ++ .../message/data/packet/PacketSequence.java | 172 +++ .../data/packet/PacketSequenceAssembler.java | 191 +++ .../packet/PacketSequenceMessageFactory.java | 226 ++++ .../message/data/terminator/Terminator.java | 48 + .../decode/dmr/message/data/usb/USBData.java | 127 ++ .../type/AbsoluteChannelParameters.java | 136 ++ .../dmr/message/type/AbstractStructure.java | 58 + .../dmr/message/type/AcknowledgeType.java | 48 + .../decode/dmr/message/type/Activity.java | 84 ++ .../dmr/message/type/AnnouncementType.java | 112 ++ .../dmr/message/type/ApplicationType.java | 73 ++ .../dmr/message/type/DataPacketFormat.java | 83 ++ .../decode/dmr/message/type/DataType.java | 128 ++ .../dmr/message/type/DefinedDataFormat.java | 113 ++ .../decode/dmr/message/type/Gateway.java | 80 ++ .../module/decode/dmr/message/type/LCSS.java | 71 ++ .../module/decode/dmr/message/type/Model.java | 47 + .../module/decode/dmr/message/type/PAR.java | 69 + .../dmr/message/type/PositionError.java | 72 ++ .../decode/dmr/message/type/ProtectKind.java | 73 ++ .../decode/dmr/message/type/Reason.java | 109 ++ .../dmr/message/type/ServiceAccessPoint.java | 103 ++ .../dmr/message/type/ServiceFunction.java | 60 + .../decode/dmr/message/type/ServiceKind.java | 62 + .../dmr/message/type/ServiceOptions.java | 136 ++ .../decode/dmr/message/type/ServiceType.java | 63 + .../dmr/message/type/SystemIdentityCode.java | 153 +++ .../type/UnifiedDataTransportFormat.java | 83 ++ .../decode/dmr/message/type/Vendor.java | 99 ++ .../decode/dmr/message/type/Version.java | 64 + .../dmr/message/voice/DMRAudioModule.java | 339 +++++ .../module/decode/dmr/message/voice/EMB.java | 183 +++ .../dmr/message/voice/VoiceAMessage.java | 41 + .../dmr/message/voice/VoiceEMBMessage.java | 92 ++ .../dmr/message/voice/VoiceMessage.java | 79 ++ .../decode/event/DecodeEventHistory.java | 74 ++ .../event/DecodeEventHistoryPreloadData.java | 40 + .../event/DecodeEventHistoryRequest.java | 35 + .../event/DecodeEventHistoryResponse.java | 47 + .../module/decode/event/DecodeEventModel.java | 21 +- .../module/decode/event/DecodeEventPanel.java | 37 +- .../module/decode/event/DecodeEventType.java | 2 + .../decode/event/MessageActivityModel.java | 48 +- .../decode/event/MessageActivityPanel.java | 53 +- .../fleetsync2/Fleetsync2DecoderState.java | 3 +- .../dsheirer/module/decode/ip/Packet.java | 8 +- .../decode/ip/PacketMessageFactory.java | 55 +- .../ip/UnknownCompressedHeaderPacket.java | 3 + .../module/decode/ip/UnknownPacket.java | 1 + .../module/decode/ip/ars/ARSHeader.java | 23 +- .../decode/ip/ars/ARSHeaderFactory.java | 2 +- .../module/decode/ip/ars/ARSPacket.java | 8 +- .../decode/ip/ars/DeviceDeRegistration.java | 10 + .../decode/ip/ars/DeviceRegistration.java | 180 ++- .../module/decode/ip/ars/QueryMessage.java | 10 + .../ip/ars/RegistrationAcknowledgement.java | 10 + ...Acknowledge.java => UnknownARSHeader.java} | 28 +- .../decode/ip/ars/UserDeRegistration.java | 15 +- .../ip/ars/UserDeRegistrationAcknowledge.java | 12 +- .../decode/ip/ars/UserRegistration.java | 175 ++- .../ip/ars/UserRegistrationAcknowledge.java | 10 + .../decode/ip/ars/identifier/ARSDevice.java | 64 + .../decode/ip/ars/identifier/ARSPassword.java | 64 + .../decode/ip/ars/identifier/ARSUser.java | 64 + .../ip/cellocator/AcknowledgeMessage.java | 4 +- .../ip/cellocator/ForwardedDataMessage.java | 4 +- .../ip/cellocator/GenericCommandMessage.java | 4 +- .../ip/cellocator/LocationStatusMessage.java | 7 +- .../ip/cellocator/MCGPMessageFactory.java | 4 +- .../decode/ip/cellocator/MCGPPacket.java | 5 +- .../ip/cellocator/ModularRequestMessage.java | 3 +- .../ip/cellocator/ModularResponseMessage.java | 3 +- .../cellocator/ProgrammingCommandMessage.java | 4 +- .../cellocator/ProgrammingStatusMessage.java | 4 +- .../ip/cellocator/UnknownMCGPMessage.java | 4 +- .../module/decode/ip/icmp/ICMPHeader.java | 94 ++ .../module/decode/ip/icmp/ICMPPacket.java | 119 ++ .../module/decode/ip/icmp/ICMPTypeCode.java | 139 ++ .../module/decode/ip/ipv4/IPV4Header.java | 10 +- .../module/decode/ip/ipv4/IPV4Packet.java | 11 +- .../module/decode/ip/lrrp/LRRPHeader.java | 87 ++ .../module/decode/ip/lrrp/LRRPPacket.java | 133 ++ .../module/decode/ip/lrrp/token/Heading.java | 64 + .../module/decode/ip/lrrp/token/Identity.java | 96 ++ .../decode/ip/lrrp/token/LRRPPosition.java | 64 + .../module/decode/ip/lrrp/token/Position.java | 93 ++ .../decode/ip/lrrp/token/Position3D.java | 64 + .../module/decode/ip/lrrp/token/Speed.java | 65 + .../decode/ip/lrrp/token/Timestamp.java | 84 ++ .../module/decode/ip/lrrp/token/Token.java | 96 ++ .../decode/ip/lrrp/token/TokenFactory.java | 54 + .../decode/ip/lrrp/token/TokenType.java | 105 ++ .../decode/ip/lrrp/token/Unknown23.java | 62 + .../decode/ip/lrrp/token/UnknownToken.java | 61 + .../module/decode/ip/udp/UDPHeader.java | 21 +- .../module/decode/ip/udp/UDPPacket.java | 23 +- .../module/decode/ip/udp/UDPPort.java | 64 + .../decode/ip/xcmp/NetworkFrequencyFile.java | 170 +++ .../module/decode/ip/xcmp/Repeater.java | 157 +++ .../module/decode/ip/xcmp/XCMPHeader.java | 110 ++ .../decode/ip/xcmp/XCMPMessageType.java | 78 ++ .../module/decode/ip/xcmp/XCMPPacket.java | 85 ++ .../decode/ip/xcmp/XCMPPacketFactory.java | 45 + .../module/decode/lj1200/LJ1200Decoder.java | 7 - .../decode/lj1200/LJ1200DecoderState.java | 2 +- .../decode/ltrnet/LTRNetDecoderState.java | 2 +- .../ltrnet/identifier/LtrSiteIdentifier.java | 43 + .../decode/ltrnet/message/osw/SiteId.java | 12 +- .../ltrstandard/LTRStandardDecoderState.java | 2 +- .../module/decode/mdc1200/MDCDecoder.java | 8 - .../decode/mdc1200/MDCDecoderState.java | 2 +- .../module/decode/mpt1327/MPT1327Decoder.java | 12 - .../decode/mpt1327/MPT1327DecoderState.java | 9 +- .../module/decode/mpt1327/MPT1327Message.java | 2 +- .../mpt1327/MPT1327TrafficChannelManager.java | 24 +- .../decode/p25/P25TrafficChannelManager.java | 31 +- .../decode/p25/audio/P25P2AudioModule.java | 10 +- .../decode/p25/phase1/P25P1Decoder.java | 8 - .../decode/p25/phase1/P25P1DecoderC4FM.java | 10 - .../decode/p25/phase1/P25P1DecoderLSM.java | 16 - .../decode/p25/phase1/P25P1DecoderState.java | 2 +- .../decode/p25/phase1/P25P1MessageFramer.java | 8 +- .../p25/phase1/message/hdu/HDUMessage.java | 5 +- .../p25/phase1/message/ldu/LDU1Message.java | 8 +- .../p25/phase1/message/ldu/LDU2Message.java | 8 +- .../message/pdu/packet/PacketMessage.java | 10 +- .../message/tdu/TDULinkControlMessage.java | 6 +- .../p25/phase2/DecodeConfigP25Phase2.java | 9 + .../decode/p25/phase2/P25P2Decoder.java | 8 - .../decode/p25/phase2/P25P2DecoderHDQPSK.java | 10 - .../decode/p25/phase2/P25P2DecoderState.java | 2 +- .../decode/p25/phase2/P25P2MessageFramer.java | 15 - ...ptionSynchronizationSequenceProcessor.java | 4 +- .../p25/phase2/timeslot/FacchTimeslot.java | 5 +- .../p25/phase2/timeslot/SacchTimeslot.java | 5 +- .../decode/passport/PassportDecoderState.java | 8 +- .../module/decode/tait/Tait1200Decoder.java | 9 - .../decode/tait/Tait1200DecoderState.java | 2 +- .../decode/traffic/TrafficChannelManager.java | 29 + .../demodulate/am/AMDemodulatorModule.java | 19 - .../demodulate/fm/FMDemodulatorModule.java | 13 - .../module/log/DecodeEventLogger.java | 6 - .../module/log/MessageEventLogger.java | 6 - .../dsheirer/playlist/PlaylistManager.java | 10 +- .../dsheirer/preference/UserPreferences.java | 2 +- .../identifier/TalkgroupFormatPreference.java | 11 +- .../talkgroup/DMRTalkgroupFormatter.java | 113 ++ .../io/github/dsheirer/protocol/Protocol.java | 10 +- .../record/binary/BinaryRecorder.java | 6 - .../wave/ComplexBufferWaveRecorder.java | 6 - .../ReusableComplexBufferAssembler.java | 1 - .../io/github/dsheirer/source/Source.java | 5 - .../github/dsheirer/source/SourceEvent.java | 15 + .../github/dsheirer/source/SourceManager.java | 2 +- .../SourceConfigTunerMultipleFrequency.java | 70 +- .../source/mixer/RealMixerSource.java | 7 - .../MultiFrequencyTunerChannelSource.java | 119 +- .../tuner/channel/TunerChannelSource.java | 8 +- .../AddChannelRotationActiveStateRequest.java | 49 + .../ChannelRotationMonitor.java | 128 +- .../DisableChannelRotationMonitorRequest.java | 29 + .../rotation/FrequencyLockChangeRequest.java | 94 ++ .../source/wave/ComplexWaveSource.java | 6 - .../dsheirer/source/wave/RealWaveSource.java | 6 - 436 files changed, 36258 insertions(+), 2409 deletions(-) delete mode 100644 .idea/modules.xml create mode 100644 src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java create mode 100644 src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java create mode 100644 src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java create mode 100644 src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java delete mode 100644 src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java create mode 100644 src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java create mode 100644 src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java create mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java create mode 100644 src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java create mode 100644 src/main/java/io/github/dsheirer/edac/BPTC_196_96.java create mode 100644 src/main/java/io/github/dsheirer/edac/BerlekempMassey.java delete mode 100644 src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java create mode 100644 src/main/java/io/github/dsheirer/edac/CRCDMR.java create mode 100644 src/main/java/io/github/dsheirer/edac/Hamming13.java create mode 100644 src/main/java/io/github/dsheirer/edac/Hamming16.java create mode 100644 src/main/java/io/github/dsheirer/edac/Hamming17.java create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java rename src/main/java/io/github/dsheirer/edac/{ReedSolomon_63_47_17.java => ReedSolomon_24_12_13_P25.java} (64%) create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java rename src/main/java/io/github/dsheirer/edac/{ReedSolomon_63_35_29.java => ReedSolomon_44_16_29_P25.java} (61%) rename src/main/java/io/github/dsheirer/edac/{ReedSolomon_44_16_29.java => ReedSolomon_63_35_29_P25.java} (78%) create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java create mode 100644 src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java create mode 100644 src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java create mode 100644 src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java delete mode 100644 src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java create mode 100644 src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/identifier/location/Point.java rename src/main/java/io/github/dsheirer/{module/decode/ltrnet/identifier => identifier/site}/SiteIdentifier.java (77%) create mode 100644 src/main/java/io/github/dsheirer/message/MessageHistory.java create mode 100644 src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java create mode 100644 src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java create mode 100644 src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/HistoryModule.java create mode 100644 src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/IDMRBurstDetectListener.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/TimeslotAlignmentTracker.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/channel/ITimeslotFrequencyReceiver.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRDecodeEvent.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRLocation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRNetwork.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRRadio.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRSite.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRToneIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRUnitStatus.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/CACH.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRBurst.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/UnknownDMRMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/IDLEMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/SlotType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/UnknownDataMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1Rate.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1_2Rate.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock3_4Rate.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/UnknownDataBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/UnknownCSBKMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAdjacentSiteInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTPreamble.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTSiteState.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAloha.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusCSBKO_60.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowAnnouncement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusNeighbors.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusPreamble.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSystemStatus.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusCSBKO_16.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowAnnouncement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusNeighborReport.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusOTAAnnouncement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTalkgroupAffiliation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTerminateChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Aloha.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/MoveTSCC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Preamble.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Protect.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/Acknowledge.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/AcknowledgeStatus.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/RegistrationAccepted.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/Ahoy.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/AuthenticateRegisterRadioCheck.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/CancelCall.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/ServiceRadioCheck.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/StunReviveKill.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/UnknownAhoy.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AdjacentSiteInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceChannelFrequency.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceWithdrawTSCC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/Announcement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/CallTimerParameters.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/LocalTime.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/MassRegistration.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/VoteNowAdvice.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/BroadcastTalkgroupVoiceChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/ChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateDataChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateVoiceChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateDataChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateVoiceChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupDataChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupVoiceChannelGrant.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ConfirmedDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DefinedShortDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/MBCHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/OctetDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PacketSequenceHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ProprietaryDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/RawShortDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ResponseDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ShortDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/StatusDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UnconfirmedDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/hytera/HyteraProprietaryDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MNISProprietaryDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MotorolaProprietaryDataHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/AbstractVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GPSInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TerminatorData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnitToUnitVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnknownFullLCMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraFullLC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraGroupVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraTerminator.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraUnitToUnitVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ActivityUpdateMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/CapacityPlusRestChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusControlChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusTrafficChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ControlChannelSystemParameters.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/HyteraXPTChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/NullMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/SLCAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/UnknownShortLCMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCContinuationBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MultiCSBK.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/UnknownMultiCSBK.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/DMRPacketMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/usb/USBData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbsoluteChannelParameters.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbstractStructure.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AcknowledgeType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Activity.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ApplicationType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataPacketFormat.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DefinedDataFormat.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Gateway.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/LCSS.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Model.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PAR.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PositionError.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ProtectKind.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Reason.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceAccessPoint.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceFunction.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceKind.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/SystemIdentityCode.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/UnifiedDataTransportFormat.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Vendor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Version.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/EMB.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceEMBMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryPreloadData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryResponse.java rename src/main/java/io/github/dsheirer/module/decode/ip/ars/{DeviceRegistrationAcknowledge.java => UnknownARSHeader.java} (70%) create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSDevice.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSPassword.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSUser.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPPacket.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPTypeCode.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPPacket.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Heading.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Identity.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/LRRPPosition.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position3D.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Speed.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Timestamp.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Token.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Unknown23.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/UnknownToken.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPort.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/NetworkFrequencyFile.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/Repeater.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPMessageType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacket.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacketFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/LtrSiteIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/traffic/TrafficChannelManager.java create mode 100644 src/main/java/io/github/dsheirer/preference/identifier/talkgroup/DMRTalkgroupFormatter.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/channel/rotation/AddChannelRotationActiveStateRequest.java rename src/main/java/io/github/dsheirer/source/tuner/channel/{ => rotation}/ChannelRotationMonitor.java (51%) create mode 100644 src/main/java/io/github/dsheirer/source/tuner/channel/rotation/DisableChannelRotationMonitorRequest.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/channel/rotation/FrequencyLockChangeRequest.java diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 2c06cff8..2eb640e2 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -4,6 +4,8 @@ bahroo broadcastify correlator + deinterleave + descrambled dinf esnid fleetsync diff --git a/.idea/misc.xml b/.idea/misc.xml index 08f2781a..6746d627 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b386659b..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 10b6dc28..8db480ff 100644 --- a/build.gradle +++ b/build.gradle @@ -110,6 +110,7 @@ idea { } javafx { + version = "13.0.2" modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing'] } diff --git a/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java b/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java index 39a13842..681ccc9c 100644 --- a/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java +++ b/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java @@ -93,12 +93,6 @@ public class AliasActionManager extends Module implements IMessageListener, List //No actions neeeded } - @Override - public void dispose() - { - //No actions neeeded - } - /** * IMessageListener interface ... delegates to this class implementation of Listener */ diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java index 54233f64..54318179 100644 --- a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java +++ b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java @@ -36,6 +36,8 @@ public enum RadioFormat { APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215", "APCO25 valid range is 0 to 16,777,215"), + DMR("********", 0, 0xFFFFFF, "0 to 16,777,215", + "DMR unit id valid range is 0 to 16,777,215"), PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607", "PASSPORT valid range is 0 to 8,388,607"), UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215", @@ -112,6 +114,8 @@ public enum RadioFormat { case APCO25: return APCO25; + case DMR: + return DMR; case PASSPORT: return PASSPORT; default: diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java index 3dc8f333..56cb74c6 100644 --- a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java +++ b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java @@ -25,6 +25,7 @@ package io.github.dsheirer.alias.id.radio; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter; import io.github.dsheirer.protocol.Protocol; import org.slf4j.Logger; @@ -32,7 +33,6 @@ import org.slf4j.LoggerFactory; import java.text.ParseException; import java.util.EnumMap; -import java.util.HashMap; import java.util.Map; /** @@ -46,6 +46,7 @@ public class RadioFormatter static { mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter()); + mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter()); mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter()); } diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java index 3ecc3adf..2dcebb7c 100644 --- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java +++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java @@ -34,8 +34,10 @@ import io.github.dsheirer.protocol.Protocol; */ public enum TalkgroupFormat { - APCO25("********", 0, 0xFFFFFF, "0 to 65,535", + APCO25("********", 0, 0xFFFF, "0 to 65,535", "APCO25 talkgroup valid range is 0 to 65,535"), + DMR("********", 0, 0xFFFFFF, "0 to 16,777,215", + "DMR talkgroup valid range is 0 to 16,777,215"), FLEETSYNC("###-####", 0, 0x7FFFFF, "001-0001 to 127-8192", "Fleetsync valid ranges are 1-127(prefix)
and 1-8192(ident) (ie. 001-0001 to 127-8192)"), LTR("##-###", 0x101, 0x3FFF, "01-001 to 20-255", diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java index 7e1b94d2..fe0bc823 100644 --- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java +++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java @@ -23,6 +23,7 @@ package io.github.dsheirer.alias.id.talkgroup; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter; @@ -35,7 +36,6 @@ import org.slf4j.LoggerFactory; import java.text.ParseException; import java.util.EnumMap; -import java.util.HashMap; import java.util.Map; /** @@ -49,6 +49,7 @@ public class TalkgroupFormatter static { mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter()); + mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter()); mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter()); LTRTalkgroupFormatter ltr = new LTRTalkgroupFormatter(); mFormatterMap.put(Protocol.LTR, ltr); diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index f4bc74c7..770c4d17 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -36,16 +36,17 @@ import org.slf4j.LoggerFactory; public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener { private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class); - private static final int DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz - + public static final long DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz + public static final int DEFAULT_TIMESLOT = 0; private final int mMaxSegmentAudioSampleLength; private Listener mAudioSegmentListener; - protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection(); + protected MutableIdentifierCollection mIdentifierCollection; private Broadcaster mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>(); private AliasList mAliasList; private AudioSegment mAudioSegment; private int mAudioSampleCount = 0; private boolean mRecordAudioOverride; + private int mTimeslot; /** * Constructs an abstract audio module @@ -53,22 +54,30 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen * @param aliasList for aliasing identifiers * @param maxSegmentAudioSampleLength in milliseconds */ - public AbstractAudioModule(AliasList aliasList, long maxSegmentAudioSampleLength) + public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentAudioSampleLength) { mAliasList = aliasList; mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples + mTimeslot = timeslot; + mIdentifierCollection = new MutableIdentifierCollection(getTimeslot()); mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection); } /** - * Constructs an abstract audio module + * Constructs an abstract audio module with a default maximum audio segment length and a default timeslot 0. */ public AbstractAudioModule(AliasList aliasList) { - this(aliasList, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH); + this(aliasList, DEFAULT_TIMESLOT, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH); } - protected abstract int getTimeslot(); + /** + * Timeslot for this audio module + */ + protected int getTimeslot() + { + return mTimeslot; + } /** * Closes the current audio segment diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index 93bc363e..13d3d2e3 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -83,9 +83,9 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis * @param aliasList for aliasing identifiers * @param maxAudioSegmentLength in milliseconds */ - public AudioModule(AliasList aliasList, long maxAudioSegmentLength) + public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength) { - super(aliasList, maxAudioSegmentLength); + super(aliasList, timeslot, maxAudioSegmentLength); } /** @@ -102,13 +102,6 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis return 0; } - @Override - public void dispose() - { - removeAudioSegmentListener(); - mSquelchStateListener = null; - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java index 5e298ae9..43cd7098 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java @@ -33,9 +33,9 @@ public abstract class AmbeAudioModule extends JmbeAudioModule private static final String AMBE_CODEC = "AMBE 3600 x 2450"; private static boolean sLibraryStatusLogged = false; - public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList) + public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot) { - super(userPreferences, aliasList); + super(userPreferences, aliasList, timeslot); if(!sLibraryStatusLogged) { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java index 35004da5..7e99f432 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java @@ -35,7 +35,7 @@ public abstract class ImbeAudioModule extends JmbeAudioModule public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList) { - super(userPreferences, aliasList); + super(userPreferences, aliasList, DEFAULT_TIMESLOT); if(!sLibraryStatusLogged) { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java index cc5565e9..cf387e85 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java @@ -54,14 +54,22 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis private IAudioCodec mAudioCodec; private UserPreferences mUserPreferences; - public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList) + public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot) { - super(aliasList); + super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH); mUserPreferences = userPreferences; - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); loadConverter(); } + @Override + public void dispose() + { + super.dispose(); + MyEventBus.getGlobalEventBus().unregister(this); + mAudioCodec = null; + } + protected IAudioCodec getAudioCodec() { return mAudioCodec; @@ -236,10 +244,4 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis mAudioCodec = null; } } - - @Override - public void dispose() - { - mAudioCodec = null; - } } diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java index 459a9a2f..a947933f 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java @@ -77,11 +77,6 @@ public abstract class MBECallSequenceRecorder extends Module implements IMessage { } - @Override - public void dispose() - { - } - /** * Writes an MBE call sequence recording to the recording directory * @param sequence to write diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java index 8b132cda..913090dc 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java @@ -129,7 +129,7 @@ public class AudioChannelPanel extends JPanel implements Listener, S public void dispose() { //Deregister from receiving preference update notifications - MyEventBus.getEventBus().unregister(this); + MyEventBus.getGlobalEventBus().unregister(this); if(mAudioOutput != null) { @@ -141,7 +141,7 @@ public class AudioChannelPanel extends JPanel implements Listener, S private void init() { //Register to receive preference updates - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); setLayout(new MigLayout("align center center, insets 0 0 0 0", "[][][align right]0[grow,fill]", "")); diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java index 42d84de5..7738927f 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -168,7 +168,7 @@ public abstract class AudioOutput implements LineListener, Listener @Override public void actionPerformed(ActionEvent e) { - MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)); + MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)); } }); popup.add(outputMenu); diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java index 58387c1b..fa281094 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java @@ -73,7 +73,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr public AudioPlaybackManager(UserPreferences userPreferences) { mUserPreferences = userPreferences; - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration(); @@ -254,6 +254,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr public void dispose() { + MyEventBus.getGlobalEventBus().unregister(this); if(mProcessingTask != null) { mProcessingTask.cancel(true); diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java index 1b9acb83..af2b1b94 100644 --- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java +++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java @@ -834,6 +834,24 @@ public class BinaryMessage extends BitSet return value; } + /** + * Returns the twos-complement value of the bits between start and end, inclusive + * @param start bit position + * @param end bit position + * @return twos complement value + */ + public int getTwosComplement(int start, int end) + { + BinaryMessage sub = getSubMessage(start, end); + boolean negative = sub.get(start); + sub.flip(0, sub.size()); + + int value = sub.getInt(0, sub.size()) + 1; + value *= (negative ? -1 : 1); + return value; + } + + /** * Returns the long value represented by the bit range. This method will * parse the bits in big endian or little endian format. The start value diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java index 4322dbad..a5abee55 100644 --- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java +++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java @@ -33,6 +33,10 @@ public class SoftSyncDetector implements ISyncProcessor mThreshold = threshold; } + public SoftSyncDetector(long pattern, int threshold) { + mPattern = pattern; + mThreshold = threshold; + } public void dispose() { mListener = null; diff --git a/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java b/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java index e93a4234..ddb88d4d 100644 --- a/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java +++ b/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java @@ -25,9 +25,13 @@ import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.sample.Listener; import net.miginfocom.swing.MigLayout; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; import javax.swing.text.DefaultCaret; -import java.awt.*; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java new file mode 100644 index 00000000..794e4e88 --- /dev/null +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java @@ -0,0 +1,60 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.channel.metadata; + +import io.github.dsheirer.controller.channel.Channel; + +import java.util.List; + +/** + * Channel and associated channel metadata + */ +public class ChannelAndMetadata +{ + private Channel mChannel; + private List mChannelMetadata; + + /** + * Connstructs an instance + * @param channel + * @param metadata + */ + public ChannelAndMetadata(Channel channel, List metadata) + { + mChannel = channel; + mChannelMetadata = metadata; + } + + /** + * Channel + */ + public Channel getChannel() + { + return mChannel; + } + + /** + * Channel Metadata + */ + public List getChannelMetadata() + { + return mChannelMetadata; + } +} diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java index 2b142889..f98f774a 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java @@ -28,6 +28,7 @@ import io.github.dsheirer.eventbus.MyEventBus; 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; @@ -57,10 +58,11 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel private List mChannelMetadata = new ArrayList(); private Map mMetadataChannelMap = new HashMap(); + private Listener mChannelAddListener; public ChannelMetadataModel() { - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); } /** @@ -82,21 +84,66 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel } } - public void add(Collection channelMetadatas, Channel channel) + public void dispose() + { + MyEventBus.getGlobalEventBus().unregister(this); + } + + /** + * Lookup model row for the specified channel metadata + */ + public int getRow(ChannelMetadata channelMetadata) + { + if(mChannelMetadata.contains(channelMetadata)) + { + return mChannelMetadata.indexOf(channelMetadata); + } + + return -1; + } + + /** + * Registers the listener to be notified when channel metadata(s) are added to the model. + * @param listener to receive channel associated with the channel metadata(s) that were added + */ + public void setChannelAddListener(Listener listener) + { + mChannelAddListener = listener; + } + + public void add(ChannelAndMetadata channelAndMetadata) { //Execute on the swing thread to avoid threading issues EventQueue.invokeLater(() -> { - for(ChannelMetadata channelMetadata: channelMetadatas) + for(ChannelMetadata channelMetadata: channelAndMetadata.getChannelMetadata()) { mChannelMetadata.add(channelMetadata); - mMetadataChannelMap.put(channelMetadata, channel); + mMetadataChannelMap.put(channelMetadata, channelAndMetadata.getChannel()); int index = mChannelMetadata.indexOf(channelMetadata); fireTableRowsInserted(index, index); channelMetadata.setUpdateEventListener(ChannelMetadataModel.this); } + + if(mChannelAddListener != null) + { + mChannelAddListener.receive(channelAndMetadata); + } }); } + /** + * Updates the mapping of channel metadata to the channel used to create the channel metadatas (and processing chain) + * @param channelMetadatas + * @param channel + */ + public void updateChannelMetadataToChannelMap(Collection channelMetadatas, Channel channel) + { + for(ChannelMetadata channelMetadata: channelMetadatas) + { + mMetadataChannelMap.put(channelMetadata, channel); + } + } + public void remove(ChannelMetadata channelMetadata) { //Execute on the swing thread to avoid threading issues diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java index 4043c540..661739d4 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java @@ -38,6 +38,8 @@ import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.swing.JLabel; import javax.swing.JMenuItem; @@ -60,6 +62,8 @@ import java.util.Map; public class ChannelMetadataPanel extends JPanel implements ListSelectionListener { + private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class); + private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel"; private ChannelModel mChannelModel; private ChannelProcessingManager mChannelProcessingManager; @@ -70,6 +74,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene private Map mBackgroundColors = new EnumMap<>(State.class); private Map mForegroundColors = new EnumMap<>(State.class); private JTableColumnWidthMonitor mTableColumnMonitor; + private Channel mUserSelectedChannel; /** * Table view for currently decoding channel metadata @@ -91,6 +96,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") ); mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel()); + mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener()); DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class); renderer.setHorizontalAlignment(SwingConstants.CENTER); @@ -165,9 +171,10 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene if(selectedMetadata != null) { - Channel selectedChannel = mChannelProcessingManager.getChannelMetadataModel() + mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel() .getChannelFromMetadata(selectedMetadata); - processingChain = mChannelProcessingManager.getProcessingChain(selectedChannel); + + processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel); } } @@ -422,4 +429,39 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene } } } + + /** + * Listener to be notified when a channel and associated channel metadata(s) are added to the underlying + * channel metadata model. + * + * When a channel is added, it is compared the the last user selected channel and if they are the same, it + * invokes a selection event on the channel metadata row so that the channel metadata is re-selected. This is + * primarily a hack to counter-act the DMR Capacity+ REST channel rotation where a channel is converted to a + * traffic channel and the previous channel is restarted. The UI effect is that the user selected channel row + * in the Now Playing window continually loses selection over the channel and causes the user to perpetually + * chase the channel row. + */ + public class ChannelAddListener implements Listener + { + @Override + public void receive(ChannelAndMetadata channelAndMetadata) + { + if(mUserSelectedChannel != null && + mUserSelectedChannel.getChannelID() == channelAndMetadata.getChannel().getChannelID()) + { + List metadata = channelAndMetadata.getChannelMetadata(); + + if(metadata.size() > 0) + { + int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0)); + + if(modelRow >= 0) + { + int tableRow = mTable.convertRowIndexToView(modelRow); + mTable.getSelectionModel().setSelectionInterval(tableRow, tableRow); + } + } + } + } + } } diff --git a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java index 2c93486c..10a5772c 100644 --- a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java @@ -41,7 +41,7 @@ import io.github.dsheirer.source.heartbeat.IHeartbeatListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; +import java.util.List; public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider, IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider, @@ -53,15 +53,35 @@ public abstract class AbstractChannelState extends Module implements IChannelEve protected Listener mDecodeEventListener; protected Listener mDecoderStateListener; protected Listener mExternalSourceEventListener; - protected Channel mChannel; + private Channel mChannel; protected boolean mSourceOverflow = false; private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver(); + /** + * Constructs an instance + * @param channel configuration + */ public AbstractChannelState(Channel channel) { mChannel = channel; } + /** + * Updates/replaces the current channel configuration with the argument. + */ + protected void updateChannelConfiguration(Channel channel) + { + mChannel = channel; + } + + /** + * Channel configuration for this channel state + */ + protected Channel getChannel() + { + return mChannel; + } + /** * Invoked each time that a heartbeat is received so that sub-class implementations can check current timers and * adjust channel state as necessary. The heartbeat arrives on a periodic basis independent of any decoded @@ -69,7 +89,7 @@ public abstract class AbstractChannelState extends Module implements IChannelEve */ protected abstract void checkState(); - public abstract Collection getChannelMetadata(); + public abstract List getChannelMetadata(); public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification); diff --git a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java index f594003c..4a0f933c 100644 --- a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java @@ -24,10 +24,8 @@ 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.identifier.string.StringIdentifier; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.protocol.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,7 +76,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { - if(event.getEvent() == Event.RESET) + if(event.getEvent() == Event.REQUEST_RESET) { getIdentifierCollection().update(mChannelNameIdentifier); } @@ -93,7 +91,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState @Override public void start() { - broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE)); + broadcast(new DecoderStateEvent(this, Event.REQUEST_ALWAYS_UNSQUELCH, State.IDLE)); getIdentifierCollection().update(mChannelNameIdentifier); } diff --git a/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java b/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java index f885b73b..91240529 100644 --- a/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java +++ b/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java @@ -41,7 +41,7 @@ public class ChangeChannelTimeoutEvent extends DecoderStateEvent */ public ChangeChannelTimeoutEvent(Object source, ChannelType channelType, long timeout, int timeslot) { - super(source, Event.CHANGE_CALL_TIMEOUT, State.IDLE, timeslot); + super(source, Event.REQUEST_CHANGE_CALL_TIMEOUT, State.IDLE, timeslot); mChannelType = channelType; mCallTimeoutMilliseconds = timeout; } diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java index fbb24a8b..a50c8f6e 100644 --- a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java @@ -125,14 +125,6 @@ public abstract class DecoderState extends AbstractDecoderState */ public abstract void init(); - /** - * Disposes any resources or pointers held by this instance to prepare for - * garbage collection - */ - public void dispose() - { - } - /** * Listens for configuration identifiers and adds them to this decoder state's identifier collection. * @return diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java index 2edb5ff5..e824d19d 100644 --- a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java @@ -22,16 +22,12 @@ package io.github.dsheirer.channel.state; +/** + * State and Status request and notification events used for communication between Decoder States and Channel states + * and by peripheral entities like the ChannelRotationMonitor. + */ public class DecoderStateEvent { - private static final DecoderStateEvent ACTIVE_STATE_TIMESLOT_0 = - new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_ACTIVE_STATE, State.ACTIVE, 0); - private static final DecoderStateEvent ACTIVE_STATE_TIMESLOT_1 = - new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_ACTIVE_STATE, State.ACTIVE, 1); - private static final DecoderStateEvent INACTIVE_STATE_TIMESLOT_0 = - new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_INACTIVE_STATE, State.ACTIVE, 0); - private static final DecoderStateEvent INACTIVE_STATE_TIMESLOT_1 = - new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_INACTIVE_STATE, State.ACTIVE, 1); private Object mSource; private Event mEvent; private State mState; @@ -63,65 +59,14 @@ public class DecoderStateEvent } /** - * Creates a channel state active event for the specified timeslot - * @param timeslot that the state applies to - * @return new event + * Creates a decoder state notification event for a null source with the specified state and timeslot. + * @param state (current) of the decoder + * @param timeslot that is reporting state + * @return new decoder state event */ - public static DecoderStateEvent activeState(int timeslot) + public static DecoderStateEvent stateNotification(State state, int timeslot) { - if(timeslot == 0) - { - return ACTIVE_STATE_TIMESLOT_0; - } - else if(timeslot == 1) - { - return ACTIVE_STATE_TIMESLOT_1; - } - else - { - throw new IllegalArgumentException("Only timeslots 0 and 1 are currently supported"); - } - } - - /** - * Creates a channel state active event for the specified timeslot. - * @param state currently - * @return new event - */ - public static DecoderStateEvent activeState() - { - return activeState(0); - } - - /** - * Creates a channel state inactive event for the specified timeslot - * @param timeslot that the state applies to - * @return new event - */ - public static DecoderStateEvent inactiveState(int timeslot) - { - if(timeslot == 0) - { - return INACTIVE_STATE_TIMESLOT_0; - } - else if(timeslot == 1) - { - return INACTIVE_STATE_TIMESLOT_1; - } - else - { - throw new IllegalArgumentException("Only timeslots 0 and 1 are currently supported"); - } - } - - /** - * Creates a channel state active event for the specified timeslot. - * @param state currently - * @return new event - */ - public static DecoderStateEvent inactiveState() - { - return inactiveState(0); + return new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_STATE, state, timeslot); } public String toString() @@ -164,15 +109,23 @@ public class DecoderStateEvent public enum Event { - ALWAYS_UNSQUELCH, - CHANGE_CALL_TIMEOUT, + //Decode state discrete events CONTINUATION, DECODE, END, + START, + + NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_ACTIVE_STATE, NOTIFICATION_CHANNEL_INACTIVE_STATE, - RESET, - SOURCE_FREQUENCY, - START; + NOTIFICATION_SOURCE_FREQUENCY, + + + REQUEST_ALWAYS_UNSQUELCH, + REQUEST_CHANGE_CALL_TIMEOUT, + REQUEST_RESET, + + //Request to convert the current channel to a traffic channel (used by DMR Cap+) + REQUEST_CONVERT_TO_TRAFFIC_CHANNEL; } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java b/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java new file mode 100644 index 00000000..50433244 --- /dev/null +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.channel.state; + +import java.util.HashMap; +import java.util.Map; + +/** + * Cache for frequently reused decoder state notification events and timeslots + */ +public class DecoderStateNotificationEventCache +{ + private Map> mTimeslotMap = new HashMap<>(); + + public DecoderStateNotificationEventCache() + { + } + + /** + * Creates a decoder state notification event for the state and timeslot, or reuses a previously created event. + * @param state for the decoder + * @param timeslot of the decoder + * @return state notification event + */ + public DecoderStateEvent getStateNotificationEvent(State state, int timeslot) + { + Map timeslotStateMap = mTimeslotMap.get(timeslot); + + if(timeslotStateMap == null) + { + timeslotStateMap = new HashMap<>(); + mTimeslotMap.put(timeslot, timeslotStateMap); + } + + DecoderStateEvent event = timeslotStateMap.get(state); + + if(event == null) + { + event = DecoderStateEvent.stateNotification(state, timeslot); + timeslotStateMap.put(state, event); + } + + return event; + } +} diff --git a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java index 1377dede..b9dd869c 100644 --- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java @@ -21,12 +21,13 @@ */ package io.github.dsheirer.channel.state; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.channel.metadata.ChannelMetadata; import io.github.dsheirer.channel.state.DecoderStateEvent.Event; import io.github.dsheirer.controller.channel.Channel; -import io.github.dsheirer.controller.channel.Channel.ChannelType; +import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification; import io.github.dsheirer.controller.channel.ChannelEvent; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierUpdateListener; @@ -39,7 +40,6 @@ import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentif import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier; import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier; import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier; -import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.event.IDecodeEvent; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.ISourceEventListener; @@ -50,11 +50,28 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; +/** + * Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel + * and provides squelch control and decoder state reset events. + * + * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode + * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates + * to the user that the activity has stopped while continuing to provide details about the call, before the state is + * reset to IDLE. + * + * State Descriptions: + * IDLE: Normal state. No voice or data call activity + * CALL/DATA/ENCRYPTED/CONTROL: Decoding states. + * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new + * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to + * signal to the user that the call is ended, while continuing to display the call details for the user + * TEARDOWN: Indicates a traffic channel that will be torn down for reuse. + */ public class MultiChannelState extends AbstractChannelState implements IDecoderStateEventListener, ISourceEventListener, IdentifierUpdateListener, IStateMachineListener { @@ -70,33 +87,23 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS private Map mIdentifierCollectionMap = new TreeMap<>(); private Map mStateMachineMap = new TreeMap<>(); private Map mSquelchControllerMap = new TreeMap<>(); - private int mTimeslotCount; + private int[] mTimeslots; + private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache(); private Listener mIdentifierUpdateListener = new IdentifierUpdateListenerProxy(); /** - * Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel - * and provides squelch control and decoder state reset events. - * - * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode - * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates - * to the user that the activity has stopped while continuing to provide details about the call, before the state is - * reset to IDLE. - * - * State Descriptions: - * IDLE: Normal state. No voice or data call activity - * CALL/DATA/ENCRYPTED/CONTROL: Decoding states. - * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new - * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to - * signal to the user that the call is ended, while continuing to display the call details for the user - * TEARDOWN: Indicates a traffic channel that will be torn down for reuse. + * Constructs an instance + * @param channel configuration + * @param aliasModel for channel metadata and identifiers + * @param timeslots array of timeslot numbers to use */ - public MultiChannelState(Channel channel, AliasModel aliasModel, int timeslots) + public MultiChannelState(Channel channel, AliasModel aliasModel, int[] timeslots) { super(channel); - mTimeslotCount = timeslots; + mTimeslots = timeslots; - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: timeslots) { mChannelMetadataMap.put(timeslot, new ChannelMetadata(aliasModel, timeslot)); MutableIdentifierCollection mutableIdentifierCollection = new MutableIdentifierCollection(timeslot); @@ -113,36 +120,45 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS mSquelchControllerMap.put(timeslot, squelchController); stateMachine.addListener(squelchController); - stateMachine.setChannelType(mChannel.getChannelType()); stateMachine.setIdentifierUpdateListener(mutableIdentifierCollection); stateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY); - if(channel.getChannelType() == ChannelType.STANDARD) - { - stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY); - } - else - { - stateMachine.setFadeTimeoutBufferMilliseconds(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000); - } + stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY); } + configureChannelType(channel); + createConfigurationIdentifiers(channel); } + /** + * Configure items according to channel type + * @param channel configuration + */ + private void configureChannelType(Channel channel) + { + for(int timeslot: mTimeslots) + { + StateMachine stateMachine = mStateMachineMap.get(timeslot); + stateMachine.setChannelType(channel.getChannelType()); + } + } + + /** + * Receive notification that the underlying channel configuration has changed. + * @param notification + */ + @Subscribe + public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification) + { + updateChannelConfiguration(notification.getChannel()); + configureChannelType(notification.getChannel()); + createConfigurationIdentifiers(notification.getChannel()); + } + @Override public void stateChanged(State state, int timeslot) { - //Broadcast current channel state so that channel rotation monitor can track - if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state)) - { - broadcast(DecoderStateEvent.activeState(timeslot)); - } - else - { - broadcast(DecoderStateEvent.inactiveState(timeslot)); - } - - ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.create(state); + ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.get(state); mIdentifierCollectionMap.get(timeslot).update(stateIdentifier); mChannelMetadataMap.get(timeslot).receive(new IdentifierUpdateNotification(stateIdentifier, IdentifierUpdateNotification.Operation.ADD, timeslot)); @@ -153,7 +169,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS mStateMachineMap.get(timeslot).setState(State.IDLE); break; case TEARDOWN: - if(mChannel.isTrafficChannel()) + if(getChannel().isTrafficChannel()) { checkTeardown(); } @@ -191,9 +207,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS if(teardown && !active) { - if(mChannel.isTrafficChannel()) + if(getChannel().isTrafficChannel()) { - broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE)); + broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE)); } else { @@ -230,7 +246,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS */ private void createConfigurationIdentifiers(Channel channel) { - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: mTimeslots) { MutableIdentifierCollection identifierCollection = mIdentifierCollectionMap.get(timeslot); @@ -334,9 +350,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS /** * Channel metadata for this channel. */ - public Collection getChannelMetadata() + public List getChannelMetadata() { - return mChannelMetadataMap.values(); + return new ArrayList<>(mChannelMetadataMap.values()); } /** @@ -345,7 +361,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS @Override public void reset() { - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: mTimeslots) { reset(timeslot); } @@ -356,7 +372,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS private void reset(int timeslot) { mStateMachineMap.get(timeslot).setState(State.RESET); - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE, timeslot)); + broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE, timeslot)); MutableIdentifierCollection identifierCollection = mIdentifierCollectionMap.get(timeslot); identifierCollection.remove(IdentifierClass.USER); } @@ -364,7 +380,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS @Override public void start() { - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: mTimeslots) { mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers(); mStateMachineMap.get(timeslot).setState(State.RESET); @@ -380,12 +396,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS } } - public void dispose() - { - mDecodeEventListener = null; - mDecoderStateListener = null; - } - @Override public Listener getSourceEventListener() { @@ -480,9 +490,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS //Rebroadcast source frequency change events for the decoder(s) to process long frequency = sourceEvent.getValue().longValue(); - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: mTimeslots) { - broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, + broadcast(new DecoderStateEvent(this, Event.NOTIFICATION_SOURCE_FREQUENCY, mStateMachineMap.get(timeslot).getState(), frequency)); //Create a new frequency configuration identifier so that downstream consumers receive the change @@ -497,12 +507,13 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS case NOTIFICATION_MEASURED_FREQUENCY_ERROR: //Rebroadcast frequency error measurements to external listener if we're currently //in an active (ie sync locked) state. - for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) + for(int timeslot: mTimeslots) { if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState())) { broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), - mChannel.getChannelType().name())); + getChannel().getChannelType().name())); + return; } } break; @@ -522,10 +533,10 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS { switch(event.getEvent()) { - case ALWAYS_UNSQUELCH: + case REQUEST_ALWAYS_UNSQUELCH: mSquelchControllerMap.get(event.getTimeslot()).setSquelchLock(true); break; - case CHANGE_CALL_TIMEOUT: + case REQUEST_CHANGE_CALL_TIMEOUT: if(event instanceof ChangeChannelTimeoutEvent) { ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event; @@ -538,19 +549,18 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState())) { mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); + + //Broadcast current channel/timeslot state so that channel rotation monitor can track + broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot())); } break; case END: - if(mChannel.isTrafficChannel()) - { - mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); - } - else - { - mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); - } + mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); + + //Broadcast current channel/timeslot state so that channel rotation monitor can track + broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot())); break; - case RESET: + case REQUEST_RESET: /* Channel State does not respond to reset events */ break; default: diff --git a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java index 6b8da41b..c532eef2 100644 --- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java @@ -21,12 +21,13 @@ */ package io.github.dsheirer.channel.state; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.channel.metadata.ChannelMetadata; import io.github.dsheirer.channel.state.DecoderStateEvent.Event; import io.github.dsheirer.controller.channel.Channel; -import io.github.dsheirer.controller.channel.Channel.ChannelType; +import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification; import io.github.dsheirer.controller.channel.ChannelEvent; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierUpdateListener; @@ -51,7 +52,6 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -97,6 +97,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder private ChannelMetadata mChannelMetadata; private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES); private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0); + private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache(); public SingleChannelState(Channel channel, AliasModel aliasModel) { @@ -107,10 +108,21 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder mStateMachine.addListener(this); mStateMachine.addListener(mSquelchController); - mStateMachine.setChannelType(mChannel.getChannelType()); mStateMachine.setIdentifierUpdateListener(mIdentifierCollection); mStateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY); - if(channel.getChannelType() == ChannelType.STANDARD) + + configureChannelType(channel); + } + + /** + * Configure items according to channel type + * @param channel configuration + */ + private void configureChannelType(Channel channel) + { + mStateMachine.setChannelType(getChannel().getChannelType()); + + if(channel.isStandardChannel()) { mStateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY); } @@ -125,36 +137,38 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder } } + /** + * Receive notification that the underlying channel configuration has changed. + * @param notification + */ + @Subscribe + public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification) + { + updateChannelConfiguration(notification.getChannel()); + configureChannelType(notification.getChannel()); + createConfigurationIdentifiers(notification.getChannel()); + } + @Override public void stateChanged(State state, int timeslot) { - //Broadcast current channel state so that channel rotation monitor can track - if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state)) - { - broadcast(DecoderStateEvent.activeState(timeslot)); - } - else - { - broadcast(DecoderStateEvent.inactiveState(timeslot)); - } - - ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.create(state); + ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.get(state); mIdentifierCollection.update(stateIdentifier); mChannelMetadata.receive(new IdentifierUpdateNotification(stateIdentifier, IdentifierUpdateNotification.Operation.ADD, timeslot)); switch(state) { case IDLE: - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE)); + broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE)); break; case RESET: reset(); mStateMachine.setState(State.IDLE); break; case TEARDOWN: - if(mChannel.isTrafficChannel()) + if(getChannel().isTrafficChannel()) { - broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE)); + broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE)); } else { @@ -258,7 +272,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder /** * Channel metadata for this channel. */ - public Collection getChannelMetadata() + public List getChannelMetadata() { return Collections.singletonList(mChannelMetadata); } @@ -270,7 +284,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder public void reset() { mStateMachine.setState(State.RESET); - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE)); + broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE)); mIdentifierCollection.remove(IdentifierClass.USER); sourceOverflow(false); } @@ -280,7 +294,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder { mIdentifierCollection.broadcastIdentifiers(); - if(mChannel.getChannelType() == ChannelType.TRAFFIC) + if(getChannel().isTrafficChannel()) { mStateMachine.setState(State.ACTIVE); } @@ -292,12 +306,6 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder mSquelchController.setSquelchLock(false); } - public void dispose() - { - mDecodeEventListener = null; - mDecoderStateListener = null; - } - @Override public Listener getSourceEventListener() { @@ -372,7 +380,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder case NOTIFICATION_FREQUENCY_CHANGE: //Rebroadcast source frequency change events for the decoder(s) to process long frequency = sourceEvent.getValue().longValue(); - broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, mStateMachine.getState(), frequency)); + broadcast(new DecoderStateEvent(this, Event.NOTIFICATION_SOURCE_FREQUENCY, mStateMachine.getState(), frequency)); //Create a new frequency configuration identifier so that downstream consumers receive the change //via channel metadata and audio packet updates - this is a silent add that is sent as a notification @@ -386,7 +394,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState())) { broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), - mChannel.getChannelType().name())); + getChannel().getChannelType().name())); } break; } @@ -405,10 +413,10 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder { switch(event.getEvent()) { - case ALWAYS_UNSQUELCH: + case REQUEST_ALWAYS_UNSQUELCH: mSquelchController.setSquelchLock(true); break; - case CHANGE_CALL_TIMEOUT: + case REQUEST_CHANGE_CALL_TIMEOUT: if(event instanceof ChangeChannelTimeoutEvent) { ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event; @@ -420,19 +428,28 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState())) { mStateMachine.setState(event.getState()); + + //Broadcast state so channel rotation manager can track + broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot())); } break; case END: - if(mChannel.isTrafficChannel()) + if(getChannel().isTrafficChannel()) { mStateMachine.setState(State.TEARDOWN); + + //Broadcast state so channel rotation manager can track + broadcast(mStateNotificationCache.getStateNotificationEvent(State.TEARDOWN, event.getTimeslot())); } else { mStateMachine.setState(State.FADE); + + //Broadcast state so channel rotation manager can track + broadcast(mStateNotificationCache.getStateNotificationEvent(State.FADE, event.getTimeslot())); } break; - case RESET: + case REQUEST_RESET: /* Channel State does not respond to reset events */ break; default: diff --git a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java index ef5b694f..f3f86e68 100644 --- a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java +++ b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java @@ -83,7 +83,7 @@ public class ControllerPanel extends JPanel { if(index == mSettingsTabIndex) { - MyEventBus.getEventBus().post(new ViewPlaylistRequest()); + MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()); } else { diff --git a/src/main/java/io/github/dsheirer/controller/channel/Channel.java b/src/main/java/io/github/dsheirer/controller/channel/Channel.java index aa00a7d8..fb54c1e7 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/Channel.java +++ b/src/main/java/io/github/dsheirer/controller/channel/Channel.java @@ -390,6 +390,8 @@ public class Channel extends Configuration implements Listener sb.append(hasSite() ? getSite() : "SITE"); sb.append("_"); sb.append(getName()); + sb.append("_"); + sb.append(getChannelID()); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java new file mode 100644 index 00000000..ab179b34 --- /dev/null +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java @@ -0,0 +1,47 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.controller.channel; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Notification that a channel configuration has changed. + */ +public class ChannelConfigurationChangeNotification extends ModuleEventBusMessage +{ + private Channel mChannel; + + /** + * Constructs an instance + * @param channel current + */ + public ChannelConfigurationChangeNotification(Channel channel) + { + mChannel = channel; + } + + /** + * New channel configuration + */ + public Channel getChannel() + { + return mChannel; + } +} diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java new file mode 100644 index 00000000..60b3078b --- /dev/null +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.controller.channel; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request to convert a currently processing standard channel type to a traffic channel type. This request will + * normally be handled by the ChannelProcessingManager instance. + */ +public class ChannelConversionRequest extends ModuleEventBusMessage +{ + private Channel mCurrentChannel; + private Channel mTrafficChannel; + + /** + * Constructs an instance. + * @param currentChannel that is actively processing + * @param trafficChannel to convert to + */ + public ChannelConversionRequest(Channel currentChannel, Channel trafficChannel) + { + mCurrentChannel = currentChannel; + mTrafficChannel = trafficChannel; + } + + /** + * Channel configuration for the channel that is currently processing + */ + public Channel getCurrentChannel() + { + return mCurrentChannel; + } + + /** + * Channel configuration for the traffic channel to convert to + */ + public Channel getTrafficChannel() + { + return mTrafficChannel; + } +} diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java deleted file mode 100644 index 1666df2d..00000000 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 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 - * ***************************************************************************** - */ - -package io.github.dsheirer.controller.channel; - -import io.github.dsheirer.channel.IChannelDescriptor; -import io.github.dsheirer.identifier.IdentifierCollection; - -/** - * Channel grant event with a channel descriptor that identifies the channel that is being created - */ -public class ChannelGrantEvent extends ChannelEvent -{ - private IChannelDescriptor mChannelDescriptor; - private IdentifierCollection mIdentifierCollection; - - /** - * Constructs a channel grant event - * @param channel with setup/configuration details - * @param event to convey for the channel - * @param channelDescriptor for the channel that is to be created - * @param identifierCollection containing identifiers to preload into the channel - */ - public ChannelGrantEvent(Channel channel, Event event, IChannelDescriptor channelDescriptor, - IdentifierCollection identifierCollection) - { - super(channel, event); - mChannelDescriptor = channelDescriptor; - mIdentifierCollection = identifierCollection; - } - - /** - * Channel descriptor that contains uplink and downlink information - */ - public IChannelDescriptor getChannelDescriptor() - { - return mChannelDescriptor; - } - - /** - * Identifier collection to use in preloading the channel state for the allocated channel - */ - public IdentifierCollection getIdentifierCollection() - { - return mIdentifierCollection; - } -} diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index 283e9ddf..bd68cc6a 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -21,25 +21,24 @@ */ package io.github.dsheirer.controller.channel; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.audio.AudioSegment; -import io.github.dsheirer.channel.IChannelDescriptor; +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.controller.channel.event.ChannelStartProcessingRequest; +import io.github.dsheirer.controller.channel.event.PreloadDataContent; import io.github.dsheirer.controller.channel.map.ChannelMapModel; -import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; -import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.Module; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.module.decode.DecoderFactory; import io.github.dsheirer.module.decode.event.IDecodeEvent; -import io.github.dsheirer.module.decode.event.MessageActivityModel; import io.github.dsheirer.module.log.EventLogManager; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.record.RecorderFactory; @@ -51,6 +50,7 @@ import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.SourceManager; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; +import io.github.dsheirer.util.ThreadPool; import javafx.application.Platform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +60,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; /** * Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created @@ -83,7 +85,8 @@ public class ChannelProcessingManager implements Listener private SourceManager mSourceManager; private AliasModel mAliasModel; private UserPreferences mUserPreferences; - private List mUnTunableFrequencies = new ArrayList<>(); + private List mLoggedFrequencies = new ArrayList<>(); + private List> mDelayedChannelStartTasks = new ArrayList<>(); /** * Constructs the channel processing manager @@ -167,6 +170,7 @@ public class ChannelProcessingManager implements Listener * * @param event that requests either enable/start or disable/stop a channel. */ + @Deprecated //TODO: convert all usage to start(ChannelStartProcessingRequest request) method @Override public synchronized void receive(ChannelEvent event) { @@ -179,7 +183,7 @@ public class ChannelProcessingManager implements Listener { try { - startProcessing(event); + startProcessing(new ChannelStartProcessingRequest(event.getChannel())); } catch(ChannelException ce) { @@ -187,9 +191,9 @@ public class ChannelProcessingManager implements Listener { long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency(); - if(!mUnTunableFrequencies.contains(frequency)) + if(!mLoggedFrequencies.contains(frequency)) { - mUnTunableFrequencies.add(frequency); + mLoggedFrequencies.add(frequency); mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequency + "] - " + ce.getMessage()); } @@ -199,9 +203,9 @@ public class ChannelProcessingManager implements Listener List frequencies = ((SourceConfigTunerMultipleFrequency)channel .getSourceConfiguration()).getFrequencies(); - if(frequencies.size() > 0 && !mUnTunableFrequencies.contains(frequencies.get(0))) + if(frequencies.size() > 0 && !mLoggedFrequencies.contains(frequencies.get(0))) { - mUnTunableFrequencies.add(frequencies.get(0)); + mLoggedFrequencies.add(frequencies.get(0)); mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequencies + "] - " + ce.getMessage()); } @@ -214,40 +218,16 @@ public class ChannelProcessingManager implements Listener } break; case REQUEST_DISABLE: - if(channel.isProcessing()) - { - try - { - switch(channel.getChannelType()) - { - case STANDARD: - stopProcessing(channel, true); - break; - case TRAFFIC: - //Don't remove traffic channel processing chains - //until explicitly deleted, so that we can reuse them - stopProcessing(channel, false); - break; - default: - break; - } - } - catch(ChannelException ce) - { - mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage()); - } - } - break; case NOTIFICATION_DELETE: if(channel.isProcessing()) { try { - stopProcessing(channel, true); + stopProcessing(channel); } catch(ChannelException ce) { - mLog.error("Error stopping deleted channel [" + channel.getName() + "] - " + ce.getMessage()); + mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage()); } } break; @@ -263,7 +243,33 @@ public class ChannelProcessingManager implements Listener */ public void start(Channel channel) throws ChannelException { - startProcessing(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_ENABLE)); + startProcessing(new ChannelStartProcessingRequest(channel)); + } + + /** + * Request to start processing a channel received over the Guava event bus. + * + * Note: since this is received over the event bus, we handle any channel exceptions inside this method. + */ + @Subscribe + public void startChannelRequest(ChannelStartProcessingRequest request) + { + if(!isProcessing(request.getChannel())) + { + try + { + startProcessing(request); + } + catch(ChannelException ce) + { + if(request.isPersistentAttempt()) + { + ScheduledFuture future = ThreadPool.SCHEDULED + .schedule(new DelayedChannelStartTask(request), 500, TimeUnit.MILLISECONDS); + mDelayedChannelStartTasks.add(future); + } + } + } } /** @@ -272,24 +278,21 @@ public class ChannelProcessingManager implements Listener */ public void stop(Channel channel) throws ChannelException { - stopProcessing(channel, !channel.isTrafficChannel()); + stopProcessing(channel); } /** - * Starts a channel/processing chain - * - * @param event that requested the channel start + * Starts a channel processing + * @param request containing channel and other details + * @throws ChannelException if a source is not available for the channel */ - private void startProcessing(ChannelEvent event) throws ChannelException + private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException { - Channel channel = event.getChannel(); + Channel channel = request.getChannel(); - ProcessingChain processingChain = mProcessingChains.get(channel); - - //If we're already processing, ignore the request - if(processingChain != null && processingChain.isProcessing()) + if(isProcessing(channel)) { - throw new ChannelException("Channel is already playing"); + return; } //Ensure that we can get a source before we construct a new processing chain @@ -316,74 +319,90 @@ public class ChannelProcessingManager implements Listener throw new ChannelException("No Tuner Available"); } - if(processingChain == null) + ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel); + + //Certain decoders aggregate the decode events in the parent channel that also includes any events produced + //by the traffic channels. Establish listener registration depending on if this channel is a traffic channel + //and the request contains the parent event history, or if this is a parent channel and the request contains + //the traffic channel event history. + if(request.hasParentDecodeEventHistory()) { - processingChain = new ProcessingChain(channel, mAliasModel); - mChannelEventBroadcaster.addListener(processingChain); - - /* Register global listeners */ - for(Listener listener : mAudioSegmentListeners) - { - processingChain.addAudioSegmentListener(listener); - } - - for(Listener listener : mDecodeEventListeners) - { - processingChain.addDecodeEventListener(listener); - } - - //Add a listener to detect source error state that indicates the channel should be shutdown - processingChain.addSourceEventListener(sourceEvent -> - { - if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null) - { - Channel toShutdown = null; - - for(Map.Entry entry: mProcessingChains.entrySet()) - { - if(entry.getValue().hasSource(sourceEvent.getSource())) - { - toShutdown = entry.getKey(); - break; - } - } - - if(toShutdown != null) - { - mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]"); - - try - { - stopProcessing(toShutdown, true); - } - catch(ChannelException ce) - { - mLog.error("Error stopping channel [" + channel.getName() + "] with source error - " + - ce.getMessage()); - } - } - } - }); - - //Register this manager to receive channel events from traffic channel manager modules within - //the processing chain - processingChain.addChannelEventListener(this); - - //Register channel to receive frequency correction events to show in the spectral display (hack!) - processingChain.addFrequencyChangeListener(channel); - - /* Processing Modules */ - List modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences); - processingChain.addModules(modules); - - /* Setup message activity model with filtering */ - FilterSet messageFilter = DecoderFactory.getMessageFilters(modules); - MessageActivityModel messageModel = new MessageActivityModel(messageFilter); - processingChain.setMessageActivityModel(messageModel); - + processingChain.getDecodeEventHistory().addListener(request.getParentDecodeEventHistory()); + } + else if(request.hasChildDecodeEventHistory()) + { + request.getChildDecodeEventHistory().addListener(processingChain.getDecodeEventHistory()); } - /* Setup event logging */ + //Register to receive event bus requests/notifications + processingChain.getEventBus().register(ChannelProcessingManager.this); + + mChannelEventBroadcaster.addListener(processingChain); + + /* Register global listeners */ + for(Listener listener : mAudioSegmentListeners) + { + processingChain.addAudioSegmentListener(listener); + } + + for(Listener listener : mDecodeEventListeners) + { + processingChain.addDecodeEventListener(listener); + } + + //Add a listener to detect source error state that indicates the channel should be shutdown + processingChain.addSourceEventListener(sourceEvent -> + { + if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null) + { + Channel toShutdown = null; + + for(Map.Entry entry: mProcessingChains.entrySet()) + { + if(entry.getValue().hasSource(sourceEvent.getSource())) + { + toShutdown = entry.getKey(); + break; + } + } + + if(toShutdown != null) + { + mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]"); + + try + { + stopProcessing(toShutdown); + } + catch(ChannelException ce) + { + mLog.error("Error stopping channel [" + channel.getName() + "] with source error - " + + ce.getMessage()); + } + } + } + }); + + //Register this manager to receive channel events from traffic channel manager modules within + //the processing chain + processingChain.addChannelEventListener(this); + + //Register channel to receive frequency correction events to show in the spectral display (hack!) + processingChain.addFrequencyChangeListener(channel); + + /* Processing Modules */ + List modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences, + request.getTrafficChannelManager()); + processingChain.addModules(modules); + + //Post preload data from the request to the event bus. Modules that can handle preload data will annotate + //their processor method with @Subscribe to receive each specific preload data content class. + for(PreloadDataContent preloadDataContent: request.getPreloadDataContents()) + { + processingChain.getEventBus().post(preloadDataContent); + } + + //Setup event logging List loggers = mEventLogManager.getLoggers(channel); if(!loggers.isEmpty()) @@ -398,25 +417,22 @@ public class ChannelProcessingManager implements Listener processingChain.setSource(source); //Inject the channel identifier for traffic channels and preload user identifiers - if(channel.isTrafficChannel() && event instanceof ChannelGrantEvent) + if(channel.isTrafficChannel()) { - ChannelGrantEvent channelGrantEvent = (ChannelGrantEvent)event; - IChannelDescriptor channelDescriptor = channelGrantEvent.getChannelDescriptor(); - - IdentifierCollection identifierCollection = channelGrantEvent.getIdentifierCollection(); - - if(channelDescriptor != null) + if(request.hasChannelDescriptor() && request.hasIdentifierCollection()) { - for(int timeslot = 0; timeslot < channelDescriptor.getTimeslotCount(); timeslot++) + for(int timeslot = 0; timeslot < request.getChannelDescriptor().getTimeslotCount(); timeslot++) { DecoderLogicalChannelNameIdentifier identifier = - DecoderLogicalChannelNameIdentifier.create(channelDescriptor.toString(), channelDescriptor.getProtocol()); + DecoderLogicalChannelNameIdentifier.create(request.getChannelDescriptor().toString(), + request.getChannelDescriptor().getProtocol()); IdentifierUpdateNotification notification = new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, timeslot); processingChain.getChannelState().updateChannelStateIdentifiers(notification); //Inject scramble parameters - for(Identifier scrambleParameters: identifierCollection.getIdentifiers(Form.SCRAMBLE_PARAMETERS)) + for(Identifier scrambleParameters: request.getIdentifierCollection() + .getIdentifiers(Form.SCRAMBLE_PARAMETERS)) { //Broadcast scramble parameters to both timeslots IdentifierUpdateNotification scrambleNotification = new IdentifierUpdateNotification(scrambleParameters, @@ -426,13 +442,13 @@ public class ChannelProcessingManager implements Listener } } - for(Identifier userIdentifier : identifierCollection.getIdentifiers(IdentifierClass.USER)) + for(Identifier userIdentifier : request.getIdentifierCollection().getIdentifiers(IdentifierClass.USER)) { - if(channelDescriptor.getTimeslotCount() > 1) + if(request.getChannelDescriptor().getTimeslotCount() > 1) { //Only broadcast an identifier update for the timeslot specified in the originating collection IdentifierUpdateNotification notification = new IdentifierUpdateNotification(userIdentifier, - IdentifierUpdateNotification.Operation.ADD, channelGrantEvent.getIdentifierCollection().getTimeslot()); + IdentifierUpdateNotification.Operation.ADD, request.getIdentifierCollection().getTimeslot()); processingChain.getChannelState().updateChannelStateIdentifiers(notification); } else @@ -446,13 +462,13 @@ public class ChannelProcessingManager implements Listener } + mProcessingChains.put(channel, processingChain); processingChain.start(); + //This has to be done on the FX event thread when the playlist editor is constructed Platform.runLater(() -> channel.setProcessing(true)); - getChannelMetadataModel().add(processingChain.getChannelState().getChannelMetadata(), channel); - - mProcessingChains.put(channel, processingChain); + getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata())); mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_START)); } @@ -461,16 +477,15 @@ public class ChannelProcessingManager implements Listener * Stops the channel/processing chain. * * @param channel to stop - * @param remove set to true to remove the associated processing chain. */ - private void stopProcessing(Channel channel, boolean remove) throws ChannelException + private void stopProcessing(Channel channel) throws ChannelException { //This has to be done on the FX event thread when the playlist editor is constructed Platform.runLater(() -> channel.setProcessing(false)); if(mProcessingChains.containsKey(channel)) { - ProcessingChain processingChain = mProcessingChains.get(channel); + ProcessingChain processingChain = mProcessingChains.remove(channel); for(ChannelMetadata channelMetadata: processingChain.getChannelState().getChannelMetadata()) { @@ -480,7 +495,6 @@ public class ChannelProcessingManager implements Listener processingChain.stop(); processingChain.removeEventLoggingModules(); - processingChain.removeRecordingModules(); //Deregister channel from receive frequency correction events to show in the spectral display (hack!) @@ -488,13 +502,11 @@ public class ChannelProcessingManager implements Listener channel.resetFrequencyCorrection(); mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP)); + mChannelEventBroadcaster.removeListener(processingChain); - if(remove) - { - mChannelEventBroadcaster.removeListener(processingChain); - mProcessingChains.remove(channel); - processingChain.dispose(); - } + //Unregister for event bus requests and notifications + processingChain.getEventBus().unregister(ChannelProcessingManager.this); + processingChain.dispose(); } else { @@ -507,13 +519,21 @@ public class ChannelProcessingManager implements Listener */ public void shutdown() { + List> delayedTasks = new ArrayList<>(mDelayedChannelStartTasks); + + for(ScheduledFuture delayedTask: delayedTasks) + { + delayedTask.cancel(true); + mDelayedChannelStartTasks.remove(delayedTask); + } + List channelsToStop = new ArrayList<>(mProcessingChains.keySet()); for(Channel channel : channelsToStop) { try { - stopProcessing(channel, true); + stopProcessing(channel); } catch(ChannelException ce) { @@ -522,6 +542,41 @@ public class ChannelProcessingManager implements Listener } } + /** + * Process a request to convert a currently processing standard channel type to a traffic channel type. + * @param request from the currently processing channel's processing chain event bus + */ + @Subscribe + public void convertToTrafficChannel(ChannelConversionRequest request) + { + //Update the channel to processing chain map. + ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel()); + + if(processingChain != null) + { + //Remove the traffic channel manager from this processing chain. Reuse or reinsertion of the traffic + //channel manager to another processing chain is handled separately. + processingChain.removeTrafficChannelManager(); + + //Update processing flag for each configuration. + Platform.runLater(() -> { + request.getCurrentChannel().setProcessing(false); + request.getTrafficChannel().setProcessing(true); + }); + + mProcessingChains.put(request.getTrafficChannel(), processingChain); + mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(), + request.getTrafficChannel()); + + //Post a change notification so that processing chain modules can reconfigure + processingChain.channelConfigurationChanged(new ChannelConfigurationChangeNotification(request.getTrafficChannel())); + } + else + { + mLog.warn("Request to convert to traffic channel ignored - no processing chain was found"); + } + } + /** * Adds a message listener that will be added to all channels to receive * any messages. @@ -571,4 +626,31 @@ public class ChannelProcessingManager implements Listener { mChannelEventBroadcaster.removeListener(listener); } + + /** + * Task to scheduling attempt to start a channel after previous attempts failed for lack of tuner channel + */ + public class DelayedChannelStartTask implements Runnable + { + private ChannelStartProcessingRequest mRequest; + + public DelayedChannelStartTask(ChannelStartProcessingRequest request) + { + mRequest = request; + } + + @Override + public void run() + { + try + { + mDelayedChannelStartTasks.remove(this); + startChannelRequest(mRequest); + } + catch(Throwable t) + { + mLog.error("Error executing persistent channel start task"); + } + } + } } diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java new file mode 100644 index 00000000..eecd22d1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java @@ -0,0 +1,228 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.controller.channel.event; + +import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.module.ModuleEventBusMessage; +import io.github.dsheirer.module.decode.event.DecodeEventHistory; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Request to start/enable processing for the specified channel + */ +public class ChannelStartProcessingRequest extends ModuleEventBusMessage +{ + private final Channel mChannel; + private final IChannelDescriptor mChannelDescriptor; + private final IdentifierCollection mIdentifierCollection; + private final TrafficChannelManager mTrafficChannelManager; + private final List> mPreloadDataContents = new ArrayList<>(); + private DecodeEventHistory mParentDecodeEventHistory; + private DecodeEventHistory mChildDecodeEventHistory; + private boolean mPersistentAttempt; + + /** + * Constructs an instance + * @param channel to start + * @param channelDescriptor that identifies which channel in a multi-channel config to start + * @param identifierCollection to use for the new channel + * @param trafficChannelManager (optional) to use for the new channel + */ + public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor, + IdentifierCollection identifierCollection, + TrafficChannelManager trafficChannelManager) + { + mChannel = channel; + mChannelDescriptor = channelDescriptor; + mIdentifierCollection = identifierCollection; + mTrafficChannelManager = trafficChannelManager; + } + + /** + * Constructs an instance + * @param channel to start + * @param channelDescriptor that identifies which channel in a multi-channel config to start + * @param identifierCollection to use for the new channel + */ + public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor, + IdentifierCollection identifierCollection) + { + this(channel, channelDescriptor, identifierCollection, null); + } + + /** + * Constructs an instance + * @param channel to start processing + * @param trafficChannelManager optional to use in the processing chain + */ + public ChannelStartProcessingRequest(Channel channel, TrafficChannelManager trafficChannelManager) + { + this(channel, null, null, trafficChannelManager); + } + + /** + * Constructs an instance + * @param channel to start processing + */ + public ChannelStartProcessingRequest(Channel channel) + { + this(channel, null, null, null); + } + + /** + * Sets a flag to persistently attempt to start this channel. When set to true, the channel processing manager + * will repeatedly try to (re)start this channel while the application continues to run. + * @param persistentAttempt if this start request should be continuously attempted until successful. + */ + public void setPersistentAttempt(boolean persistentAttempt) + { + mPersistentAttempt = persistentAttempt; + } + + /** + * Indicates if this request should be persistently attempted until the channel processing is started. + */ + public boolean isPersistentAttempt() + { + return mPersistentAttempt; + } + + /** + * Channel to start processing + * @return channel to start + */ + public Channel getChannel() + { + return mChannel; + } + + /** + * Optional traffic channel manager to (re)use. + * @return traffic channel manager or null + */ + public TrafficChannelManager getTrafficChannelManager() + { + return mTrafficChannelManager; + } + + /** + * Optional parent decode event history module to be added as a decode event listener + */ + public DecodeEventHistory getParentDecodeEventHistory() + { + return mParentDecodeEventHistory; + } + + /** + * Indicates if this request has a parent decode event history + */ + public boolean hasParentDecodeEventHistory() + { + return mParentDecodeEventHistory != null; + } + + /** + * Sets the parent decode event history + * @param parentHistory + */ + public void setParentDecodeEventHistory(DecodeEventHistory parentHistory) + { + mParentDecodeEventHistory = parentHistory; + } + + /** + * Optional child decode event history module + */ + public DecodeEventHistory getChildDecodeEventHistory() + { + return mChildDecodeEventHistory; + } + + /** + * Indicates if this request has a child decode event history + */ + public boolean hasChildDecodeEventHistory() + { + return mChildDecodeEventHistory != null; + } + + /** + * Sets the child decode event history + * @param childHistory + */ + public void setChildDecodeEventHistory(DecodeEventHistory childHistory) + { + mChildDecodeEventHistory = childHistory; + } + + /** + * Channel descriptor to use for the started channel + */ + public IChannelDescriptor getChannelDescriptor() + { + return mChannelDescriptor; + } + + /** + * Indicates if this request has a non-null channel descriptor + */ + public boolean hasChannelDescriptor() + { + return mChannelDescriptor != null; + } + + /** + * Identifier collection to use for preloading the channel with identifiers + */ + public IdentifierCollection getIdentifierCollection() + { + return mIdentifierCollection; + } + + /** + * Indicates if this request has a non-null identifier collection + */ + public boolean hasIdentifierCollection() + { + return mIdentifierCollection != null; + } + + /** + * Adds data content to be preloaded onto the event bus at startup. + * @param content to broadcast. + */ + public void addPreloadDataContent(PreloadDataContent content) + { + mPreloadDataContents.add(content); + } + + /** + * Data content that should be broadcast on the processing chain event bus at startup. + */ + public List> getPreloadDataContents() + { + return mPreloadDataContents; + } +} diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java b/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java new file mode 100644 index 00000000..e9d635a0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java @@ -0,0 +1,58 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.controller.channel.event; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Data that should be preloaded into a processing chain by broadcasting the data onto the processing chain so that + * any module can subscribe to it. + * @param data generic that should be loaded into modules in the processing chain. + */ +public abstract class PreloadDataContent extends ModuleEventBusMessage +{ + private final T mData; + + /** + * Constructs an instance + * @param data to preload + */ + public PreloadDataContent(T data) + { + mData = data; + } + + /** + * Data to preload + * @return + */ + public T getData() + { + return mData; + } + + /** + * Indicates if the data payload is non-null + */ + public boolean hasData() + { + return mData != null; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index a3897c40..235f8581 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -599,9 +599,9 @@ public class FilterFactory * @param alpha - roll-off factor. * @return - filter coefficients */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha) + public static float[] getRootRaisedCosine(double samplesPerSymbol, int symbolCount, float alpha) { - int taps = samplesPerSymbol * symbolCount; + int taps = (int)(samplesPerSymbol * symbolCount); float scale = 0; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index eaaf131b..6af2eea3 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -22,8 +22,8 @@ package io.github.dsheirer.dsp.filter.design; +import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; @@ -83,41 +83,52 @@ public class FilterViewer extends Application // .passBandRipple(0.01) // .build(); - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(50000.0) - .passBandCutoff(6500) - .passBandAmplitude(1.0) - .passBandRipple(0.005) - .stopBandAmplitude(0.0) - .stopBandStart(7200) - .stopBandRipple(0.01) - .build(); +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(50000.0) +// .passBandCutoff(6500) +// .passBandAmplitude(1.0) +// .passBandRipple(0.005) +// .stopBandAmplitude(0.0) +// .stopBandStart(7200) +// .stopBandRipple(0.01) +// .build(); float[] taps = null; -// try -// { -// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19); -// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); -// } -// catch(FilterDesignException fde) -// { -// mLog.error("Error"); -// } + boolean rrc = false; - try + if(rrc) { - RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + double sampleRate = 50000.0; + int symbolRate = 4800; + double samplesPerSymbol = sampleRate / symbolRate / 2; + float alpha = 0.25f; + int symbolCount = 20; - if(designer.isValid()) + taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol, symbolCount, alpha); + } + else + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(50000) + .passBandCutoff(4500) + .passBandAmplitude(1.0) + .passBandRipple(0.1) + .stopBandAmplitude(0.0) + .stopBandStart(7000) + .stopBandRipple(0.01) + .build(); + + try { - taps = designer.getImpulseResponse(); + taps = FilterFactory.getTaps(specification);// + } + catch(Exception fde) //FilterDesignException + { + mLog.error("Couldn't design low pass baseband filter for sample rate"); } } - catch(FilterDesignException fde) - { - mLog.error("Filter design error", fde); - } + if(taps == null) { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java new file mode 100644 index 00000000..36bb653c --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java @@ -0,0 +1,35 @@ +package io.github.dsheirer.dsp.filter.design; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.stage.Stage; + +public class FilterViewerLauncher +{ + private JFXPanel mJFXPanel; + + public FilterViewerLauncher() + { + mJFXPanel = new JFXPanel(); + + Platform.runLater(() -> + { + FilterViewer filterViewer = new FilterViewer(); + Stage stage = new Stage(); + + try + { + filterViewer.start(stage); + } + catch(Exception e) + { + e.printStackTrace(); + } + }); + } + + public static void main(String[] args) + { + FilterViewerLauncher filterViewerLauncher = new FilterViewerLauncher(); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java index 9a1ef20a..8c13b300 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java @@ -24,22 +24,32 @@ package io.github.dsheirer.dsp.symbol; public enum Dibit { - D01_PLUS_3(false, true, 1, 4), - D00_PLUS_1(false, false, 0, 0), - D10_MINUS_1(true, false, 2, 8), - D11_MINUS_3(true, true, 3, 12); + D01_PLUS_3(false, true, 1, 4, 1), + D00_PLUS_1(false, false, 0, 0, 0), + D10_MINUS_1(true, false, 2, 8, 2), + D11_MINUS_3(true, true, 3, 12, 3); private boolean mBit1; private boolean mBit2; private int mLowValue; private int mHighValue; + private int mValue; - private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue) + private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value) { mBit1 = bit1; mBit2 = bit2; mLowValue = lowValue; mHighValue = highValue; + mValue = value; + } + + /** + * Binary value of the symbol + */ + public int getValue() + { + return mValue; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java new file mode 100644 index 00000000..25131e03 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java @@ -0,0 +1,260 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.dsp.symbol; + +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Circular buffer for storing and accessing dibits. + */ +public class DibitDelayBuffer +{ + private final static Logger mLog = LoggerFactory.getLogger(DibitDelayBuffer.class); + + protected Dibit[] mBuffer; + protected int mPointer; + + /** + * Constructs a dibit delay buffer of the specified length + */ + public DibitDelayBuffer(int length) + { + Validate.isTrue(length > 0, "Dibit buffer length must be greater than 0"); + mBuffer = new Dibit[length]; + reset(); + } + + /** + * Resets the buffer and preloads the buffer with all zeros + */ + public void reset() + { + for(int x = 0; x < mBuffer.length; x++) + { + mBuffer[x] = Dibit.D00_PLUS_1; + } + } + + /** + * Length of this circular buffer + */ + public int length() + { + return mBuffer.length; + } + + /** + * Returns the dibit at the specified index + * @param index to get + * @return dibit at the specified index + */ + public Dibit get(int index) + { + if(index < 0 || index >= mBuffer.length) + { + throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]"); + } + + return mBuffer[index]; + } + + /** + * Sets the dibit at the specified index + * @param index of the dibit + * @param dibit to load at the specified index + */ + public void set(int index, Dibit dibit) + { + if(index < 0 || index >= mBuffer.length) + { + throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]"); + } + + mBuffer[index] = dibit; + } + + /** + * Returns an ordered view of the internal circular buffer contents. + */ + public Dibit[] getBuffer() + { + Dibit[] transferBuffer = new Dibit[mBuffer.length]; + + int transferBufferPointer = 0; + int bufferPointer = mPointer; + + while(transferBufferPointer < transferBuffer.length) + { + transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++]; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + + return transferBuffer; + } + + /** + * Retrieves a view of the buffer from the starting index to the specified length. Note: if the requested + * length exceeds the length of this buffer, the contents will be unwrapped from the circular buffer and repeated. + * @param start of the view where 0 is the first element. + * @param length of the dibit view + * @return an array of dibits + */ + public Dibit[] getBuffer(int start, int length) + { + Dibit[] transferBuffer = new Dibit[length]; + + int transferBufferPointer = 0; + int bufferPointer = (mPointer + start) % mBuffer.length; + + while(transferBufferPointer < transferBuffer.length) + { + transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++]; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + + return transferBuffer; + } + + /** + * Extracts a corrected binary message from the dibit buffer + * + * @param start dibit index where 0 is the oldest dibit and the newest dibit is buffer length - 1 + * @param dibitLength number of dibits to include in the message, making the message length = dibitLength * 2 + * @return corrected binary message + */ + public CorrectedBinaryMessage getMessage(int start, int dibitLength) + { + CorrectedBinaryMessage message = new CorrectedBinaryMessage(dibitLength * 2); + + int dibitCount = 0; + int bufferPointer = (mPointer + start) % mBuffer.length; + + try + { + while(dibitCount < dibitLength) + { + Dibit dibit = mBuffer[bufferPointer++]; + message.add(dibit.getBit1()); + message.add(dibit.getBit2()); + dibitCount++; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + } + catch(BitSetFullException e) + { + //This shouldn't happen + mLog.error("Bit set full ??", e); + } + + return message; + } + + /** + * Utility method to view the buffer contents with the dibits decoded as an array of zeros and ones. + */ + public int[] getBufferAsArray() + { + Dibit[] dibits = getBuffer(); + + int[] bits = new int[dibits.length * 2]; + + for(int x = 0; x < dibits.length; x++) + { + if(dibits[x].getBit1()) + { + bits[x * 2] = 1; + } + if(dibits[x].getBit2()) + { + bits[x * 2 + 1] = 1; + } + } + + return bits; + } + + /** + * Places the dibit into the internal circular buffer, overwriting the oldest dibit. + */ + public void put(Dibit dibit) + { + mBuffer[mPointer++] = dibit; + + if(mPointer >= mBuffer.length) + { + mPointer = 0; + } + } + + /** + * Places the dibit into the internal circular buffer, overwriting and returning the + * oldest dibit. + */ + public Dibit getAndPut(Dibit dibit) + { + Dibit toReturn = mBuffer[mPointer]; + put(dibit); + return toReturn; + } + + /** + * Logs the contents of the buffer + */ + public void log() + { + StringBuilder sb = new StringBuilder(); + + int counter = 0; + int pointer = mPointer; + + while(counter < mBuffer.length) + { + sb.append(mBuffer[pointer].getBit1() ? "1" : "0"); + sb.append(mBuffer[pointer++].getBit2() ? "1" : "0"); + + if(pointer >= mBuffer.length) + { + pointer = 0; + } + + counter++; + } + mLog.debug("BUFFER: " + sb + " Length:" + mBuffer.length); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java b/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java new file mode 100644 index 00000000..1da8c892 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java @@ -0,0 +1,120 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.dsp.symbol; + +/** + * QPSK carrier lock elements that indicate if a PLL is locked normally, or if it has locked at +/- 90 degrees, + * or inverted 180 degrees. Abnormal PLL lock is normally detected by rotated sync patterns. + * + * Each enumeration entry provides a correct(dibit) method that supports correcting dibits that were captured with a + * misaligned PLL lock to what they should be if the PLL were locked correctly. This allows detected (yet misaligned) + * messages to become usable through dibit correction even though the PLL was misaligned. + */ +public enum QPSKCarrierLock +{ + /** + * Normal PLL lock indication + */ + NORMAL + { + @Override + public Dibit correct(Dibit dibit) + { + //No correction needed + return dibit; + } + }, + + /** + * PLL lock at plus 90 degrees to the carrier, requiring a PLL adjustment of -90 degrees or -PI/2. + */ + PLUS_90 + { + @Override + public Dibit correct(Dibit dibit) + { + switch(dibit) + { + case D00_PLUS_1: + return Dibit.D10_MINUS_1; + case D01_PLUS_3: + return Dibit.D00_PLUS_1; + case D10_MINUS_1: + return Dibit.D11_MINUS_3; + case D11_MINUS_3: + default: + return Dibit.D01_PLUS_3; + } + } + }, + + /** + * PLL lock at minus 90 degrees to the carrier, requiring a PLL adjustment of +90 degrees, or +PI/2. + */ + MINUS_90 + { + @Override + public Dibit correct(Dibit dibit) + { + switch(dibit) + { + case D00_PLUS_1: + return Dibit.D01_PLUS_3; + case D01_PLUS_3: + return Dibit.D11_MINUS_3; + case D10_MINUS_1: + return Dibit.D00_PLUS_1; + case D11_MINUS_3: + default: + return Dibit.D10_MINUS_1; + } + } + }, + + /** + * PLL lock at 180 degrees or inverted to the carrier, requiring a PLL adjustment of 180 degrees or PI. + */ + INVERTED + { + @Override + public Dibit correct(Dibit dibit) + { + switch(dibit) + { + case D00_PLUS_1: + return Dibit.D11_MINUS_3; + case D01_PLUS_3: + return Dibit.D10_MINUS_1; + case D10_MINUS_1: + return Dibit.D01_PLUS_3; + case D11_MINUS_3: + default: + return Dibit.D00_PLUS_1; + } + } + }; + + /** + * Method for each entry to provide dibit correction solutions + * @param dibit that was generated by a misaligned QPSK Phase locked loop + * @return dibit that is corrected to the dibit for a correctly aligned QPSK Phase Locked Loop + */ + public abstract Dibit correct(Dibit dibit); +} diff --git a/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java b/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java index e6eb9e5a..aac7e24a 100644 --- a/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java +++ b/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java @@ -1,106 +1,106 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + package io.github.dsheirer.edac; import io.github.dsheirer.bits.BinaryMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * 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 - ******************************************************************************/ - -public class BCH_63_16_11 extends BerlekempMassey_63 +public class BCH_63_16_11 extends ReedSolomon_63_P25 { - private final static Logger mLog = LoggerFactory.getLogger( BCH_63_16_11.class ); + private final static Logger mLog = LoggerFactory.getLogger(BCH_63_16_11.class); - /** - * BCH( 63,16,11) decoder - */ - public BCH_63_16_11() + /** + * BCH( 63,16,11) decoder + */ + public BCH_63_16_11() { - /* TT = 11 = maximum correctable bit errors */ - super( 11 ); + super(63, 41); } - - /** - * Performs error detection and correction on the first 63 bits of the - * message argument. If the message is correctable, only the first 16 bits - * (information bits) are corrected. - * - * @return - true = success, false = failure - */ - public BinaryMessage correctNID( BinaryMessage message ) - { - CRC status = CRC.PASSED; - - int[] original = message.toReverseIntegerArray( 0, 62 ); - int[] corrected = new int[ 63 ]; - boolean irrecoverableErrors = decode( original, corrected ); + /** + * Performs error detection and correction on the first 63 bits of the + * message argument. If the message is correctable, only the first 16 bits + * (information bits) are corrected. + * + * @return - true = success, false = failure + */ + public BinaryMessage correctNID(BinaryMessage message) + { + CRC status = CRC.PASSED; - if( irrecoverableErrors ) - { - message.setCRC( CRC.FAILED_CRC ); - - return message; - } - else - { - for( int x = 0; x < 16; x++ ) - { - int index = 63 - x - 1; - - if( corrected[ index ] != original[ index ] ) - { - status = CRC.CORRECTED; - - if( corrected[ index ] == 1 ) - { - message.set( x ); - } - else - { - message.clear( x ); - } - } - } - } - - message.setCRC( status ); - - return message; - } - - public static void main( String[] args ) - { - String orig = "0010011000000011010010100000000110000111110011101010001010110000"; - String error = "0001010100000011010010100000000110000111100011001010001010110000"; + int[] original = message.toReverseIntegerArray(0, 62); + int[] corrected = new int[63]; - BinaryMessage errorMessage = BinaryMessage.load( error ); + boolean irrecoverableErrors = decode(original, corrected); - BCH_63_16_11 bch = new BCH_63_16_11(); + if(irrecoverableErrors) + { + message.setCRC(CRC.FAILED_CRC); - mLog.debug( "ORIG:" + orig ); - mLog.debug( " ERR:" + errorMessage.toString() ); - mLog.debug( " " ); + return message; + } + else + { + for(int x = 0; x < 16; x++) + { + int index = 63 - x - 1; - errorMessage = bch.correctNID( errorMessage ); - - mLog.debug( " CRC: " + errorMessage.getCRC().name() ); - mLog.debug( "CORR:" + errorMessage.toString() ); - mLog.debug( "ORIG:" + orig ); - } + if(corrected[index] != original[index]) + { + status = CRC.CORRECTED; + + if(corrected[index] == 1) + { + message.set(x); + } + else + { + message.clear(x); + } + } + } + } + + message.setCRC(status); + + return message; + } + + public static void main(String[] args) + { + String orig = "0010011000000011010010100000000110000111110011101010001010110000"; + String error = "0001010100000011010010100000000110000111100011001010001010110000"; + + BinaryMessage errorMessage = BinaryMessage.load(error); + + BCH_63_16_11 bch = new BCH_63_16_11(); + + mLog.debug("ORIG:" + orig); + mLog.debug(" ERR:" + errorMessage.toString()); + mLog.debug(" "); + + errorMessage = bch.correctNID(errorMessage); + + mLog.debug(" CRC: " + errorMessage.getCRC().name()); + mLog.debug("CORR:" + errorMessage.toString()); + mLog.debug("ORIG:" + orig); + } } diff --git a/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java b/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java new file mode 100644 index 00000000..c405a3c6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java @@ -0,0 +1,62 @@ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/* + Adopted from https://github.com/boatbod/op25/blob/e5e8fe457240fb358a8163bcf6fe7dda5e4d8392/op25/gr-op25_repeater/lib/hamming.cc + */ +public class BPTC_17_12_3 { + + public static boolean decode17123(BinaryMessage d, int offset) + { + if(d==null) { + return false; + } + + // Calculate the checksum this column should have + boolean c0 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(6 + offset) ^ d.get(7 + offset) ^ d.get(9 + offset); + boolean c1 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(7 + offset) ^ d.get(8 + offset) ^ d.get(10 + offset); + boolean c2 = d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(8 + offset) ^ d.get(9 + offset) ^ d.get(11 + offset); + boolean c3 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(7 + offset) ^ d.get(10 + offset); + boolean c4 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(5 + offset) ^ d.get(6 + offset) ^ d.get(8 + offset) ^ d.get(11 + offset); + + // Compare these with the actual bits + byte n = 0x00; + n |= (c0 != d.get(12 + offset)) ? 0x01 : 0x00; + n |= (c1 != d.get(13 + offset)) ? 0x02 : 0x00; + n |= (c2 != d.get(14 + offset)) ? 0x04 : 0x00; + n |= (c3 != d.get(15 + offset)) ? 0x08 : 0x00; + n |= (c4 != d.get(16 + offset)) ? 0x10 : 0x00; + + switch (n) { + // Parity bit errors + case 0x01: d.set(12 + offset, !d.get(12 + offset)); return true; + case 0x02: d.set(13 + offset, !d.get(13 + offset)); return true; + case 0x04: d.set(14 + offset, !d.get(14 + offset)); return true; + case 0x08: d.set(15 + offset, !d.get(15 + offset)); return true; + case 0x10: d.set(16 + offset, !d.get(16 + offset)); return true; + + // Data bit errors + case 0x1B: d.set(0 + offset, !d.get(0 + offset)); return true; + case 0x1F: d.set(1 + offset, !d.get(1 + offset)); return true; + case 0x17: d.set(2 + offset, !d.get(2 + offset)); return true; + case 0x07: d.set(3 + offset, !d.get(3 + offset)); return true; + case 0x0E: d.set(4 + offset, !d.get(4 + offset)); return true; + case 0x1C: d.set(5 + offset, !d.get(5 + offset)); return true; + case 0x11: d.set(6 + offset, !d.get(6 + offset)); return true; + case 0x0B: d.set(7 + offset, !d.get(7 + offset)); return true; + case 0x16: d.set(8 + offset, !d.get(8 + offset)); return true; + case 0x05: d.set(9 + offset, !d.get(9 + offset)); return true; + case 0x0A: d.set(10 + offset,!d.get(10 + offset)); return true; + case 0x14: d.set(11 + offset,!d.get(11 + offset)); return true; + + // No bit errors + case 0x00: return true; + + // Unrecoverable errors + default: return false; + } + } + +} diff --git a/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java b/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java new file mode 100644 index 00000000..cd01f677 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java @@ -0,0 +1,284 @@ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Block Product Turbo Code (BPTC) 196/96 Support + */ +public class BPTC_196_96 +{ + public static final int BPTC_LENGTH = 196; + public static final int EXTRACTED_LENGTH = 96; + public static final int MAX_ORIGINAL_INDEX = 136; + public static final int COLUMN_COUNT = 15; + public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit + public static final int CHECKSUM_COLUMN_COUNT = 4; + public static final int MESSAGE_START_INDEX = 4; + + /** + * DMR De-interleaving indices + */ + public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182, + 167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3, + 184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20, + 5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37, + 22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54, + 39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71, + 56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103, + 88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120, + 105, 90, 75, 60, 45, 30, 15}; + + /** + * DMR Column Indices + */ + public static int[][] COLUMN_INDEXES = new int[][]{ + {1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181}, + {2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182}, + {3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183}, + {4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184}, + {5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185}, + {6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186}, + {7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187}, + {8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188}, + {9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189}, + {10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190}, + {11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191}, + {12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192}, + {13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193}, + {14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194}, + {15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}}; + + /** + * Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96) + * protected message + * @param original uncorrected BPTC(196,96) message + * @return error corrected and extracted 96-bit message + */ + public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original) + { + //De-Interleave + CorrectedBinaryMessage message = deinterleave(original); + + //Perform error detection and correction + correct(message); + + //Extract the 96-bit message from the corrected 196 bits + CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH); + + //Transfer the corrected bit count + extracted.setCorrectedBitCount(message.getCorrectedBitCount()); + + int index = MESSAGE_START_INDEX; + + while(index < MAX_ORIGINAL_INDEX) + { + if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT) + { + try + { + extracted.add(message.get(index)); + } + catch(BitSetFullException bsfe) + { + //Should never get to here + bsfe.printStackTrace(); + return extracted; + } + + index++; + } + else + { + index += CHECKSUM_COLUMN_COUNT; + } + } + + return extracted; + } + + /** + * De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message + * @param message that contains interleaved (transmitted) message + * @return de-interleaved message + */ + private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message) + { + CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH); + for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++) + { + deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x])); + } + + return deinterleaved; + } + + /** + * Identifies the error indices from the row level Hamming(15,11,3) protected words + * @param message containing protected words in rows + * @return list of zero or more error indices + */ + private static List getRowErrors(CorrectedBinaryMessage message) + { + List rowErrors = new ArrayList<>(); + + int offset = 0; + int index = -1; + + for(int row = 0; row < 13; row++) + { + //The first bit of the message is a pad/reserved bit and is ignored. + offset = (row * 15) + 1; + index = Hamming15.getErrorIndex(message, offset); + + if(index >= 0) + { + rowErrors.add(index); + } + } + + //Note: row errors should already be in order and don't need sorted + return rowErrors; + } + + /** + * Identifies the error indices from the column level Hamming(13,9,3) protected words + * @param message containing protected words in columns + * @return list of zero or more error indices + */ + private static List getColumnErrors(CorrectedBinaryMessage message) + { + List columnErrors = new ArrayList<>(); + + int index = -1; + + for(int column = 0; column < 15; column++) + { + index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]); + + if(index >= 0) + { + columnErrors.add(index); + } + } + + Collections.sort(columnErrors); + return columnErrors; + } + + /** + * Performs error detection and correction on the DMR BPTC(196,96) protected message + * + * @param message to process and extract + */ + private static void correct(CorrectedBinaryMessage message) + { + List rowErrors = getRowErrors(message); + + if(!rowErrors.isEmpty()) + { + List columnErrors = getColumnErrors(message); + + if(matches(rowErrors, columnErrors)) + { + for(Integer index: rowErrors) + { + message.flip(index); + } + + message.setCorrectedBitCount(rowErrors.size()); + } + else + { + //Step 1: correct row errors and recalculate column errors (should be zero) + for(Integer index: rowErrors) + { + message.flip(index); + } + + List columnErrorsPart2 = getColumnErrors(message); + + if(columnErrorsPart2.isEmpty()) + { + //Success + message.setCorrectedBitCount(rowErrors.size()); + } + else + { + //Reverse the row error correction + for(Integer index: rowErrors) + { + message.flip(index); + } + + //Step 2: correct column errors and recalculate row errors (should be zero) + for(Integer index: columnErrors) + { + message.flip(index); + } + + List rowErrorsPart2 = getRowErrors(message); + + if(rowErrorsPart2.isEmpty()) + { + //Success + message.setCorrectedBitCount(columnErrors.size()); + } + else + { + //Reverse the column error correction + for(Integer index: columnErrors) + { + message.flip(index); + } + + //Set the error count greater than the maximum correctable column errors (16) + message.setCorrectedBitCount(16); + } + } + } + } + } + + /** + * Indicates if the errors detected by the row protected words match the errors detected by the column protected words + * @param rowErrors bit positions + * @param columnErrors bit positions + * @return true if they have the same quantity of bit position errors and the bit positions match + */ + private static boolean matches(List rowErrors, List columnErrors) + { + if(rowErrors.size() == columnErrors.size()) + { + for(int x = 0; x < rowErrors.size(); x++) + { + if(!rowErrors.get(x).equals(columnErrors.get(0))) + { + return false; + } + } + + return true; + } + else + { + return false; + } + } + + public static void main(String[] args) + { + String raw = "0000000001100010100101100011100100110100110101000100000011101001110001001000100001001010010100001000011111011000000011110000111000111111100011000011100001100000000101001010000100010111010100111001"; + System.out.println("RAW:" + raw); + + CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.load(raw)); + System.out.println("LOD:" + message.toString()); + CorrectedBinaryMessage extracted = extract(message); + System.out.println("EXT:" + extracted.toString()); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java new file mode 100644 index 00000000..1865f403 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java @@ -0,0 +1,559 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.edac; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Berlekemp Massey decoder for primitive RS/BCH block codes + * + * Original C implementation by Simon Rockliff, 26 June 1991 + */ +public class BerlekempMassey +{ + public final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class); + + /* Golay field size GF( 2 ** MM ) */ + private int MM; + + /* Codeword Length: NN = 2 ** MM - 1 */ + private int NN; + private int MAX_NN; + + /* Hamming distance between codewords: NN - KK + 1 = 2 * TT + 1 */ + private int KK; + + /* Maximum number of errors that can be corrected */ + int TT; + + int[] alpha_to; + int[] index_of; + int[] gg; + + /** + * Constructs an instance + * + * @param galoisFieldSize as a power of 2, e.g. for a GF(2^6) the parameter would be 6 + * @param maxCorrectableErrors is Hamming Distance / 2. e.g. RS(24,16,9) would be (24 - 16) / 2, or floor(9/2). + * @param generatorPolynomial for the Galois Field represented in binary as 0/1 integers where the array is in + * big-endian format. For example: polynomial a6 + a1 + 1 is 1000011, as big endian: 1100001, and in array + * form as {1,1,0,0,0,0,1} + */ + public BerlekempMassey(int galoisFieldSize, int maxCorrectableErrors, int[] generatorPolynomial) + { + //TODO: fix this + MM = galoisFieldSize; + NN = (int)FastMath.pow(2, MM) - 1; + TT = maxCorrectableErrors; + KK = NN - 2 * TT; + + } + + public BerlekempMassey(int bitSize, int n, int k, int[] generatorPolynomial) + { + Validate.isTrue(bitSize == generatorPolynomial.length - 1, + "Generator polynomial length must be: bitSize + 1"); + + MM = bitSize; + NN = n; + KK = k; + TT = (int)Math.floor((NN - KK) / 2); + + //Since this may be a shortened code, setup arrays to the max size of NN + int maxNN = (int)Math.pow(2, MM) - 1; + + alpha_to = new int[maxNN + 1]; + index_of = new int[maxNN + 1]; + + gg = new int[NN - KK + 1]; + + generate_gf(generatorPolynomial); + + gen_poly(); + } + + /** + * Generates the Golay Field. + * + * Generates a GF( 2**mm ) from the irreducible polynomial + * p(X) in pp[0]..pp[mm] + * + * Lookup tables: + * index_of[] = polynomial form + * alpha_to[] = contains j=alpha**i; + * + * Polynomial form -> Index form index_of[j=alpha**i] = i + * + * alpha_to = 2 is the primitive element of GF( 2**mm ) + * + * @param generator_polynomial + */ + private void generate_gf(int[] generator_polynomial) + { + int i; + int mask = 1; + + alpha_to[MM] = 0; + + for(i = 0; i < MM; i++) + { + alpha_to[i] = mask; + index_of[alpha_to[i]] = i; + + if(generator_polynomial[i] != 0) + { + alpha_to[MM] ^= mask; + } + + mask <<= 1; + } + + index_of[alpha_to[MM]] = MM; + + mask >>= 1; + + for(i = MM + 1; i < NN; i++) + { + if(alpha_to[i - 1] >= mask) + { + alpha_to[i] = alpha_to[MM] ^ ((alpha_to[i - 1] ^ mask) << 1); + } + else + { + alpha_to[i] = alpha_to[i - 1] << 1; + } + + index_of[alpha_to[i]] = i; + } + + index_of[0] = -1; + } + + /** + * Generates the polynomial for a TT-error correction code. + * + * Length NN = ( 2 ** MM -1 ) Reed Solomon code from the product of + * (X+alpha**i), i=1..2*tt + */ + private void gen_poly() + { + int i, j; + + gg[0] = 2; /* primitive element alpha = 2 for GF(2**mm) */ + gg[1] = 1; /* g(x) = (X+alpha) initially */ + for(i = 2; i <= (NN - KK); i++) + { + gg[i] = 1; + + for(j = i - 1; j > 0; j--) + { + if(gg[j] != 0) + { + gg[j] = gg[j - 1] ^ alpha_to[(index_of[gg[j]] + i) % NN]; + } + else + { + gg[j] = gg[j - 1]; + } + } + + /* gg[0] can never be zero */ + gg[0] = alpha_to[(index_of[gg[0]] + i) % NN]; + } + + /* convert gg[] to index form for quicker encoding */ + for(i = 0; i <= (NN - KK); i++) + { + gg[i] = index_of[gg[i]]; + } + } + + /** + * Decodes + * + * @param input + * @param output + * @return + */ + /* assume we have received bits grouped into mm-bit symbols in recd[i], + i=0..(nn-1), and recd[i] is polynomial form. + We first compute the 2*tt syndromes by substituting alpha**i into rec(X) and + evaluating, storing the syndromes in s[i], i=1..2tt (leave s[0] zero) . + Then we use the Berlekamp iteration to find the error location polynomial + elp[i]. If the degree of the elp is >tt, we cannot correct all the errors + and hence just put out the information symbols uncorrected. If the degree of + elp is <=tt, we substitute alpha**i , i=1..n into the elp to get the roots, + hence the inverse roots, the error location numbers. If the number of errors + located does not equal the degree of the elp, we have more than tt errors + and cannot correct them. Otherwise, we then solve for the error value at + the error location and correct the error. The procedure is that found in + Lin and Costello. For the cases where the number of errors is known to be too + large to correct, the information symbols as received are output (the + advantage of systematic encoding is that hopefully some of the information + symbols will be okay and that if we are in luck, the errors are in the + parity part of the transmitted codeword). Of course, these insoluble cases + can be returned as error flags to the calling routine if desired. */ + public boolean decode(final int[] input, int[] output) //input, output + { + int u, q; + int[][] elp = new int[NN - KK + 2][NN - KK]; + int[] d = new int[NN - KK + 2]; + int[] l = new int[NN - KK + 2]; + int[] u_lu = new int[NN - KK + 2]; + int[] s = new int[NN - KK + 1]; + int count = 0; + boolean syn_error = false; + int[] root = new int[TT]; + int[] loc = new int[TT]; + int[] z = new int[TT + 1]; + int[] err = new int[NN]; + int[] reg = new int[TT + 1]; + + boolean irrecoverable_error = false; + + /* put recd[i] into index form (ie as powers of alpha) */ + for(int i = 0; i < NN; i++) + { + output[i] = index_of[input[i]]; + } + + /* first form the syndromes */ + for(int i = 1; i <= NN - KK; i++) + { + s[i] = 0; + + for(int j = 0; j < NN; j++) + { + if(output[j] != -1) + { + /* recd[j] in index form */ + s[i] ^= alpha_to[(output[j] + i * j) % NN]; + } + } + + /* convert syndrome from polynomial form to index form */ + if(s[i] != 0) + { + /* set flag if non-zero syndrome => error */ + syn_error = true; + } + + s[i] = index_of[s[i]]; + } + + if(syn_error) /* if errors, try and correct */ + { + /* compute the error location polynomial via the Berlekamp iterative algorithm, + following the terminology of Lin and Costello : d[u] is the 'mu'th + discrepancy, where u='mu'+1 and 'mu' (the Greek letter!) is the step number + ranging from -1 to 2*tt (see L&C), l[u] is the + degree of the elp at that step, and u_l[u] is the difference between the + step number and the degree of the elp. + */ + + /* initialise table entries */ + d[0] = 0; /* index form */ + d[1] = s[1]; /* index form */ + elp[0][0] = 0; /* index form */ + elp[1][0] = 1; /* polynomial form */ + + for(int i = 1; i < NN - KK; i++) + { + elp[0][i] = -1; /* index form */ + elp[1][i] = 0; /* polynomial form */ + } + + l[0] = 0; + l[1] = 0; + u_lu[0] = -1; + u_lu[1] = 0; + u = 0; + + do + { + u++; + + if(d[u] == -1) + { + l[u + 1] = l[u]; + + for(int i = 0; i <= l[u]; i++) + { + elp[u + 1][i] = elp[u][i]; + elp[u][i] = index_of[elp[u][i]]; + } + } + else + /* search for words with greatest u_lu[q] for which d[q]!=0 */ + { + q = u - 1; + + while((d[q] == -1) && (q > 0)) + { + q--; + } + + /* have found first non-zero d[q] */ + if(q > 0) + { + int j = q; + + do + { + j--; + + if((d[j] != -1) && (u_lu[q] < u_lu[j])) + { + q = j; + } + } + while(j > 0); + } + ; + + /* have now found q such that d[u]!=0 and u_lu[q] is maximum */ + /* store degree of new elp polynomial */ + l[u + 1] = FastMath.max(l[u], l[q] + u - q); + + /* form new elp(x) */ + for(int i = 0; i < NN - KK; i++) + { + elp[u + 1][i] = 0; + } + + for(int i = 0; i <= l[q]; i++) + { + if(elp[q][i] != -1) + { + elp[u + 1][i + u - q] = + alpha_to[(d[u] + NN - d[q] + + elp[q][i]) % NN]; + } + } + for(int i = 0; i <= l[u]; i++) + { + elp[u + 1][i] ^= elp[u][i]; + elp[u][i] = index_of[elp[u][i]]; /*convert old elp value to index*/ + } + } + + u_lu[u + 1] = u - l[u + 1]; + + /* form (u+1)th discrepancy */ + if(u < NN - KK) /* no discrepancy computed on last iteration */ + { + if(s[u + 1] != -1) + { + d[u + 1] = alpha_to[s[u + 1]]; + } + else + { + d[u + 1] = 0; + } + for(int i = 1; i <= l[u + 1]; i++) + { + if((s[u + 1 - i] != -1) && (elp[u + 1][i] != 0)) + { + d[u + 1] ^= alpha_to[(s[u + 1 - i] + + index_of[elp[u + 1][i]]) % NN]; + } + } + + d[u + 1] = index_of[d[u + 1]]; /* put d[u+1] into index form */ + } + } + while((u < NN - KK) && (l[u + 1] <= TT)); + + u++; + + if(l[u] <= TT) /* can correct error */ + { + /* put elp into index form */ + for(int i = 0; i <= l[u]; i++) + { + elp[u][i] = index_of[elp[u][i]]; + } + + /* find roots of the error location polynomial */ + if(l[u] >= 0) + { + System.arraycopy(elp[u], 1, reg, 1, l[u]); + } + + count = 0; + + for(int i = 1; i <= NN; i++) + { + q = 1; + + for(int j = 1; j <= l[u]; j++) + { + if(reg[j] != -1) + { + reg[j] = (reg[j] + j) % NN; + q ^= alpha_to[reg[j]]; + } + ; + } + + if(q == 0) /* store root and error location number indices */ + { + root[count] = i; + loc[count] = NN - i; + count++; + } + ; + } + ; + + if(count == l[u]) /* no. roots = degree of elp hence <= tt errors */ + { + /* form polynomial z(x) */ + for(int i = 1; i <= l[u]; i++) /* Z[0] = 1 always - do not need */ + { + if((s[i] != -1) && (elp[u][i] != -1)) + { + z[i] = alpha_to[s[i]] ^ alpha_to[elp[u][i]]; + } + else if((s[i] != -1) && (elp[u][i] == -1)) + { + z[i] = alpha_to[s[i]]; + } + else if((s[i] == -1) && (elp[u][i] != -1)) + { + z[i] = alpha_to[elp[u][i]]; + } + else + { + z[i] = 0; + } + + for(int j = 1; j < i; j++) + { + if((s[j] != -1) && (elp[u][i - j] != -1)) + { + z[i] ^= alpha_to[(elp[u][i - j] + s[j]) % NN]; + } + } + + z[i] = index_of[z[i]]; /* put into index form */ + } + ; + + /* evaluate errors at locations given by error location numbers loc[i] */ + for(int i = 0; i < NN; i++) + { + err[i] = 0; + + if(output[i] != -1) /* convert recd[] to polynomial form */ + { + output[i] = alpha_to[output[i]]; + } + else + { + output[i] = 0; + } + } + + for(int i = 0; i < l[u]; i++) /* compute numerator of error term first */ + { + err[loc[i]] = 1; /* accounts for z[0] */ + + for(int j = 1; j <= l[u]; j++) + { + if(z[j] != -1) + { + err[loc[i]] ^= alpha_to[(z[j] + j * root[i]) % NN]; + } + } + + if(err[loc[i]] != 0) + { + err[loc[i]] = index_of[err[loc[i]]]; + + q = 0; /* form denominator of error term */ + + for(int j = 0; j < l[u]; j++) + { + if(j != i) + { + q += index_of[1 ^ alpha_to[(loc[j] + root[i]) % NN]]; + } + } + + q = q % NN; + err[loc[i]] = alpha_to[(err[loc[i]] - q + NN) % NN]; + output[loc[i]] ^= err[loc[i]]; /*recd[i] must be in polynomial form */ + } + } + } + else + { + /* no. roots != degree of elp => >tt errors and cannot solve */ + irrecoverable_error = true; + } + + } + else + { + /* elp has degree >tt hence cannot solve */ + irrecoverable_error = true; + } + } + else + { + /* no non-zero syndromes => no errors: output received codeword */ + for(int i = 0; i < NN; i++) + { + if(output[i] != -1) /* convert recd[] to polynomial form */ + { + output[i] = alpha_to[output[i]]; + } + else + { + output[i] = 0; + } + } + } + + if(irrecoverable_error) + { + for(int i = 0; i < NN; i++) /* could return error flag if desired */ + { + if(output[i] != -1) /* convert recd[] to polynomial form */ + { + output[i] = alpha_to[output[i]]; + } + else + { + output[i] = 0; /* just output received codeword as is */ + } + } + } + + return irrecoverable_error; + } +} diff --git a/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java b/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java deleted file mode 100644 index 6ab364ea..00000000 --- a/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.edac; - -import org.apache.commons.math3.util.FastMath; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Berlekemp Massey decoder for 63-bit primitive RS/BCH block codes - */ -public class BerlekempMassey_63 -{ - public final static Logger mLog = LoggerFactory.getLogger( BerlekempMassey_63.class ); - - /* Golay field size GF( 2 ** MM ) */ - private static final int MM = 6; - - /* Codeword Length: NN = 2 ** MM - 1 */ - private static final int NN = 63; - - /* Hamming distance between codewords: NN - KK + 1 = 2 * TT + 1 */ - private int KK; - - /* Maximum number of errors that can be corrected */ - int TT; - - int[] alpha_to; - int[] index_of; - int[] gg; - - public BerlekempMassey_63( int tt ) - { - TT = tt; - KK = NN - 2 * TT; - - alpha_to = new int[ NN + 1 ]; - index_of = new int[ NN + 1 ]; - gg = new int[ NN - KK + 1 ]; - - /* P25 generator polynomial */ - int generator_polinomial[] = { 1, 1, 0, 0, 0, 0, 1 }; - - generate_gf( generator_polinomial ); - - gen_poly(); - } - - /** - * Generates the Golay Field. - * - * Generates a GF( 2**mm ) from the irreducible polynomial - * p(X) in pp[0]..pp[mm] - * - * Lookup tables: - * index_of[] = polynomial form - * alpha_to[] = contains j=alpha**i; - * - * Polynomial form -> Index form index_of[j=alpha**i] = i - * - * alpha_to = 2 is the primitive element of GF( 2**mm ) - * - * @param generator_polynomial - */ - private void generate_gf( int[] generator_polynomial ) - { - int i; - int mask = 1; - - alpha_to[ MM ] = 0; - - for( i = 0; i < MM; i++ ) - { - alpha_to[ i ] = mask; - index_of[ alpha_to[ i ] ] = i; - - if( generator_polynomial[ i ] != 0 ) - { - alpha_to[ MM ] ^= mask; - } - - mask <<= 1; - } - - index_of[ alpha_to[ MM ] ] = MM; - - mask >>= 1; - - for ( i = MM + 1; i < NN; i++ ) - { - if( alpha_to[ i - 1 ] >= mask ) - { - alpha_to[ i ] = alpha_to[ MM ] ^ ( ( alpha_to[ i - 1 ] ^ mask ) << 1 ); - } - else - { - alpha_to[ i ] = alpha_to[ i - 1 ] << 1; - } - - index_of[ alpha_to[ i ] ] = i; - } - - index_of[ 0 ] = -1; - } - - /** - * Generates the polynomial for a TT-error correction code. - * - * Length NN = ( 2 ** MM -1 ) Reed Solomon code from the product of - * (X+alpha**i), i=1..2*tt - */ - private void gen_poly() - { - int i, j; - - gg[ 0 ] = 2; /* primitive element alpha = 2 for GF(2**mm) */ - gg[ 1 ] = 1; /* g(x) = (X+alpha) initially */ - for( i = 2; i <= NN - KK; i++ ) - { - gg[ i ] = 1; - - for( j = i - 1; j > 0; j-- ) - { - if( gg[ j ] != 0 ) - { - gg[ j ] = gg[ j - 1 ] ^ alpha_to[ ( index_of[ gg[ j ] ] + i ) % NN ]; - } - else - { - gg[ j ] = gg[ j - 1 ]; - } - } - - /* gg[0] can never be zero */ - gg[ 0 ] = alpha_to[ ( index_of[ gg[ 0 ] ] + i ) % NN ]; - } - - /* convert gg[] to index form for quicker encoding */ - for( i = 0; i <= NN - KK; i++ ) - { - gg[ i ] = index_of[ gg[ i ] ]; - } - } - - /** - * Decodes - * @param input - * @param output - * @return - */ - /* assume we have received bits grouped into mm-bit symbols in recd[i], - i=0..(nn-1), and recd[i] is polynomial form. - We first compute the 2*tt syndromes by substituting alpha**i into rec(X) and - evaluating, storing the syndromes in s[i], i=1..2tt (leave s[0] zero) . - Then we use the Berlekamp iteration to find the error location polynomial - elp[i]. If the degree of the elp is >tt, we cannot correct all the errors - and hence just put out the information symbols uncorrected. If the degree of - elp is <=tt, we substitute alpha**i , i=1..n into the elp to get the roots, - hence the inverse roots, the error location numbers. If the number of errors - located does not equal the degree of the elp, we have more than tt errors - and cannot correct them. Otherwise, we then solve for the error value at - the error location and correct the error. The procedure is that found in - Lin and Costello. For the cases where the number of errors is known to be too - large to correct, the information symbols as received are output (the - advantage of systematic encoding is that hopefully some of the information - symbols will be okay and that if we are in luck, the errors are in the - parity part of the transmitted codeword). Of course, these insoluble cases - can be returned as error flags to the calling routine if desired. */ - public boolean decode( final int[] input, int[] output ) //input, output - { - int u, q; - int[][] elp = new int[ NN - KK + 2 ][ NN - KK ]; - int[] d = new int[ NN - KK + 2 ]; - int[] l = new int[ NN - KK + 2 ]; - int[] u_lu = new int[ NN - KK + 2 ]; - int[] s = new int[ NN - KK + 1 ]; - int count = 0; - boolean syn_error = false; - int[] root = new int[ TT ]; - int[] loc = new int[ TT ]; - int[] z = new int[ TT + 1 ]; - int[] err = new int[ NN ]; - int[] reg = new int[ TT + 1 ]; - - boolean irrecoverable_error = false; - - /* put recd[i] into index form (ie as powers of alpha) */ - for( int i = 0; i < NN; i++ ) - { - output[ i ] = index_of[ input[ i ] ]; - } - - /* first form the syndromes */ - for( int i = 1; i <= NN - KK; i++ ) - { - s[ i ] = 0; - - for( int j = 0; j < NN; j++ ) - { - if( output[ j ] != -1 ) - { - /* recd[j] in index form */ - s[ i ] ^= alpha_to[ ( output[ j ] + i * j ) % NN ]; - } - } - - /* convert syndrome from polynomial form to index form */ - if( s[ i ] != 0 ) - { - /* set flag if non-zero syndrome => error */ - syn_error = true; - } - - s[ i ] = index_of[ s[ i ] ]; - } - - if( syn_error ) /* if errors, try and correct */ - { - /* compute the error location polynomial via the Berlekamp iterative algorithm, - following the terminology of Lin and Costello : d[u] is the 'mu'th - discrepancy, where u='mu'+1 and 'mu' (the Greek letter!) is the step number - ranging from -1 to 2*tt (see L&C), l[u] is the - degree of the elp at that step, and u_l[u] is the difference between the - step number and the degree of the elp. - */ - - /* initialise table entries */ - d[ 0 ] = 0; /* index form */ - d[ 1 ] = s[ 1 ]; /* index form */ - elp[ 0 ][ 0 ] = 0; /* index form */ - elp[ 1 ][ 0 ] = 1; /* polynomial form */ - - for( int i = 1; i < NN - KK; i++ ) - { - elp[ 0 ][ i ] = -1; /* index form */ - elp[ 1 ][ i ] = 0; /* polynomial form */ - } - - l[ 0 ] = 0; - l[ 1 ] = 0; - u_lu[ 0 ] = -1; - u_lu[ 1 ] = 0; - u = 0; - - do - { - u++; - - if( d[ u ] == -1 ) - { - l[ u + 1 ] = l[ u ]; - - for( int i = 0; i <= l[ u ]; i++ ) - { - elp[ u + 1 ][ i ] = elp[ u ][ i ]; - elp[ u ][ i ] = index_of[ elp[ u ][ i ] ]; - } - } - else - /* search for words with greatest u_lu[q] for which d[q]!=0 */ - { - q = u - 1; - - while( ( d[ q ] == -1 ) && ( q > 0 ) ) - { - q--; - } - - /* have found first non-zero d[q] */ - if( q > 0 ) - { - int j = q; - - do - { - j--; - - if( ( d[ j ] != -1 ) && ( u_lu[ q ] < u_lu[ j ] ) ) - { - q = j; - } - } - while( j > 0 ); - }; - - /* have now found q such that d[u]!=0 and u_lu[q] is maximum */ - /* store degree of new elp polynomial */ - l[u + 1] = FastMath.max(l[u], l[q] + u - q); - - /* form new elp(x) */ - for( int i = 0; i < NN - KK; i++ ) - { - elp[ u + 1 ][ i ] = 0; - } - - for( int i = 0; i <= l[q]; i++ ) - { - if( elp[ q ][ i ] != -1 ) - { - elp[ u + 1 ][ i + u - q ] = - alpha_to[ ( d[ u ] + NN - d[ q ] - + elp[ q ][ i ]) % NN ]; - } - } - for( int i = 0; i <= l[u]; i++ ) - { - elp[ u + 1 ][ i ] ^= elp[ u ][ i ]; - elp[ u ][ i ] = index_of[ elp[ u ][ i ] ]; /*convert old elp value to index*/ - } - } - - u_lu[ u + 1 ] = u - l[ u + 1 ]; - - /* form (u+1)th discrepancy */ - if( u < NN - KK ) /* no discrepancy computed on last iteration */ - { - if ( s[ u + 1 ] != -1 ) - { - d[ u + 1 ] = alpha_to[ s[ u + 1 ] ]; - } - else - { - d[ u + 1 ] = 0; - } - for( int i = 1; i <= l[ u + 1 ]; i++ ) - { - if( ( s[ u + 1 - i ] != -1 ) && ( elp[ u + 1 ][ i] != 0 ) ) - { - d[ u + 1 ] ^= alpha_to[ ( s[ u + 1 - i ] - + index_of[ elp[ u + 1 ][ i ] ] ) % NN ]; - } - } - - d[ u + 1 ] = index_of[ d[ u + 1 ] ]; /* put d[u+1] into index form */ - } - } - while( ( u < NN - KK ) && ( l[ u + 1 ] <= TT) ); - - u++; - - if( l[ u ] <= TT ) /* can correct error */ - { - /* put elp into index form */ - for( int i = 0; i <= l[u]; i++ ) - { - elp[ u ][ i ] = index_of[ elp[ u ][ i ] ]; - } - - /* find roots of the error location polynomial */ - if (l[u] >= 0) System.arraycopy(elp[u], 1, reg, 1, l[u]); - - count = 0; - - for( int i = 1; i <= NN; i++ ) - { - q = 1; - - for( int j = 1; j <= l[u]; j++ ) - { - if( reg[ j ] != -1 ) - { - reg[ j ] = ( reg[ j ] + j ) % NN; - q ^= alpha_to[ reg[ j ] ]; - }; - } - - if( q == 0 ) /* store root and error location number indices */ - { - root[ count ] = i; - loc[ count ] = NN - i; - count++; - }; - }; - - if( count == l[ u ] ) /* no. roots = degree of elp hence <= tt errors */ - { - /* form polynomial z(x) */ - for( int i = 1; i <= l[ u ]; i++ ) /* Z[0] = 1 always - do not need */ - { - if( ( s[ i ] != -1 ) && ( elp[ u ][ i ] != -1 ) ) - { - z[ i ] = alpha_to[ s[ i ] ] ^ alpha_to[ elp[ u ][ i ] ]; - } - else if( ( s[ i ] != -1 ) && ( elp[ u ][ i ] == -1 ) ) - { - z[ i ] = alpha_to[ s[ i ] ]; - } - else if( ( s[ i ] == -1 ) && ( elp[ u ][ i ] != -1 ) ) - { - z[ i ] = alpha_to[ elp[ u ][ i ] ]; - } - else - { - z[ i ] = 0; - } - - for( int j = 1; j < i; j++ ) - { - if( ( s[ j ] != -1 ) && ( elp[ u ][ i - j ] != -1 ) ) - { - z[ i ] ^= alpha_to[ ( elp[ u ][ i - j ] + s[ j ] ) % NN ]; - } - } - - z[ i ] = index_of[ z[ i ] ]; /* put into index form */ - }; - - /* evaluate errors at locations given by error location numbers loc[i] */ - for( int i = 0; i < NN; i++ ) - { - err[ i ] = 0; - - if( output[ i ] != -1 ) /* convert recd[] to polynomial form */ - { - output[ i ] = alpha_to[ output[ i ] ]; - } - else - { - output[ i ] = 0; - } - } - - for( int i = 0; i < l[ u ]; i++ ) /* compute numerator of error term first */ - { - err[ loc[ i ] ] = 1; /* accounts for z[0] */ - - for( int j = 1; j <= l[ u ]; j++ ) - { - if( z[ j ] != -1 ) - { - err[ loc[ i ] ] ^= alpha_to[ ( z[ j ] + j * root[ i ] ) % NN ]; - } - } - - if( err[ loc[ i ] ] != 0 ) - { - err[ loc[ i ] ] = index_of[ err[ loc[ i ] ] ]; - - q = 0; /* form denominator of error term */ - - for (int j = 0; j < l[u]; j++) - { - if (j != i) - { - q += index_of[1 ^ alpha_to[(loc[j] + root[i]) % NN]]; - } - } - - q = q % NN; - err[loc[i]] = alpha_to[(err[loc[i]] - q + NN) % NN]; - output[loc[i]] ^= err[loc[i]]; /*recd[i] must be in polynomial form */ - } - } - } - else - { - /* no. roots != degree of elp => >tt errors and cannot solve */ - irrecoverable_error = true; - } - - } - else - { - /* elp has degree >tt hence cannot solve */ - irrecoverable_error = true; - } - } - else - { - /* no non-zero syndromes => no errors: output received codeword */ - for (int i = 0; i < NN; i++) - { - if (output[i] != -1) /* convert recd[] to polynomial form */ - { - output[i] = alpha_to[output[i]]; - } - else - { - output[i] = 0; - } - } - } - - if( irrecoverable_error ) - { - for (int i = 0; i < NN; i++) /* could return error flag if desired */ - { - if (output[i] != -1) /* convert recd[] to polynomial form */ - { - output[i] = alpha_to[output[i]]; - } - else - { - output[i] = 0; /* just output received codeword as is */ - } - } - } - - return irrecoverable_error; - } -} diff --git a/src/main/java/io/github/dsheirer/edac/CRCDMR.java b/src/main/java/io/github/dsheirer/edac/CRCDMR.java new file mode 100644 index 00000000..0112995b --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/CRCDMR.java @@ -0,0 +1,290 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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 + * + ******************************************************************************/ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DMR CRC check/correction methods + */ +public class CRCDMR +{ + private final static Logger mLog = LoggerFactory.getLogger(CRCDMR.class); + + /** + * CRC-CCITT 16-bit checksums for a message length of 80 bits plus 16 + * additional checksums representing CRC checksum bit errors + * + * Generated by: + * CRCUtil.generate( 80, 16, 0x11021l, 0xFFFFl, true ); + */ + public static final int[] CCITT_80_CHECKSUMS = new int[] + { + 0xE434, 0x721A, 0x390D, 0x9496, 0x4A4B, 0xAD35, 0xDE8A, 0x6F45, 0xBFB2, + 0x5FD9, 0xA7FC, 0x53FE, 0x29FF, 0x9CEF, 0xC667, 0xEB23, 0xFD81, 0xF6D0, + 0x7B68, 0x3DB4, 0x1EDA, 0xF6D, 0x8FA6, 0x47D3, 0xABF9, 0xDDEC, 0x6EF6, + 0x377B, 0x93AD, 0xC1C6, 0x60E3, 0xB861, 0xD420, 0x6A10, 0x3508, 0x1A84, + 0xD42, 0x6A1, 0x8B40, 0x45A0, 0x22D0, 0x1168, 0x8B4, 0x45A, 0x22D, 0x8906, + 0x4483, 0xAA51, 0xDD38, 0x6E9C, 0x374E, 0x1BA7, 0x85C3, 0xCAF1, 0xED68, + 0x76B4, 0x3B5A, 0x1DAD, 0x86C6, 0x4363, 0xA9A1, 0xDCC0, 0x6E60, 0x3730, + 0x1B98, 0xDCC, 0x6E6, 0x373, 0x89A9, 0xCCC4, 0x6662, 0x3331, 0x9188, + 0x48C4, 0x2462, 0x1231, 0x8108, 0x4084, 0x2042, 0x1021, 0x1, 0x2, 0x4, 0x8, + 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, + 0x8000, + }; + + /** + * Confirmed Packet Data Unit CRC-9 checksums, generated by: + * + * long[] table = CRCUtil.generate( 135, 9, 0x259l, 0x1FF, false, Parity.NONE ); + */ + public static final int[] CRC9_CHECKSUMS = new int[] + { + 0x1E7, 0x1F3, 0x1F9, 0x1FC, 0x0D2, 0x045, 0x122, 0x0BD, 0x15E, 0x083, + 0x141, 0x1A0, 0x0FC, 0x052, 0x005, 0x102, 0x0AD, 0x156, 0x087, 0x143, + 0x1A1, 0x1D0, 0x0C4, 0x04E, 0x00B, 0x105, 0x182, 0x0ED, 0x176, 0x097, + 0x14B, 0x1A5, 0x1D2, 0x0C5, 0x162, 0x09D, 0x14E, 0x08B, 0x145, 0x1A2, + 0x0FD, 0x17E, 0x093, 0x149, 0x1A4, 0x0FE, 0x053, 0x129, 0x194, 0x0E6, + 0x05F, 0x12F, 0x197, 0x1CB, 0x1E5, 0x1F2, 0x0D5, 0x16A, 0x099, 0x14C, + 0x08A, 0x069, 0x134, 0x0B6, 0x077, 0x13B, 0x19D, 0x1CE, 0x0CB, 0x165, + 0x1B2, 0x0F5, 0x17A, 0x091, 0x148, 0x088, 0x068, 0x018, 0x020, 0x03C, + 0x032, 0x035, 0x11A, 0x0A1, 0x150, 0x084, 0x06E, 0x01B, 0x10D, 0x186, + 0x0EF, 0x177, 0x1BB, 0x1DD, 0x1EE, 0x0DB, 0x16D, 0x1B6, 0x0F7, 0x17B, + 0x1BD, 0x1DE, 0x0C3, 0x161, 0x1B0, 0x0F4, 0x056, 0x007, 0x103, 0x181, + 0x1C0, 0x0CC, 0x04A, 0x009, 0x104, 0x0AE, 0x07B, 0x13D, 0x19E, 0x0E3, + 0x171, 0x1B8, 0x0F0, 0x054, 0x006, 0x02F, 0x117, 0x18B, 0x1C5, 0x1E2, + 0x0DD, 0x16E, 0x09B, 0x14D, 0x1A6 + }; + + + /** + * Error detection and correction of single-bit errors for CCITT 16-bit CRC protected 80-bit messages. + */ + public static BinaryMessage correctCCITT80(BinaryMessage message, int messageStart, int crcStart) + { + int calculated = 0; //Starting value + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(messageStart); + i >= messageStart && i < crcStart; + i = message.nextSetBit(i + 1)) + { + calculated ^= CCITT_80_CHECKSUMS[i - messageStart]; + } + + int checksum = getIntChecksum(message, crcStart, 16); + + int residual = calculated ^ checksum; + + if(residual == 0 || residual == 0xFFFF) + { + message.setCRC(CRC.PASSED); + + return message; + } + else + { + int errorLocation = getBitError(residual, CCITT_80_CHECKSUMS); + + if(errorLocation >= 0) + { + message.flip(errorLocation + messageStart); + + message.setCRC(CRC.CORRECTED); + + return message; + } + } + + message.setCRC(CRC.FAILED_CRC); + + return message; + } + + /** + * Calculates the CRC checksum for the specified message + * @param message to check + * @param start of the message + * @param length of the message (assumes the CRC checksum follows) + * @param checksums to use in calculating the message checksum + * @return calculated checksum + */ + public static int calculate(BinaryMessage message, int start, int length, long[] checksums) + { + int calculated = 0; + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(start); i >= start && i < length; i = message.nextSetBit(i + 1)) + { + calculated ^= checksums[i - start]; + } + + return calculated; + } + + /** + * Error detection and correction of single-bit errors for CCITT 16-bit CRC protected 80-bit messages. + */ + public static int correctCCITT80(CorrectedBinaryMessage message, int messageStart, int crcStart, int mask) + { + int calculated = mask; //Starting value + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1)) + { + calculated ^= CCITT_80_CHECKSUMS[i - messageStart]; + } + + int checksum = getIntChecksum(message, crcStart, 16); + + int residual = calculated ^ checksum; + + if(residual == 0 || residual == 0xFFFF) + { + return 0; + } + else + { + int errorLocation = getBitError(residual, CCITT_80_CHECKSUMS); + + if(errorLocation >= 0) + { + message.flip(errorLocation + messageStart); + message.incrementCorrectedBitCount(1); + return 1; + } + } + + //Message has at least 2 bit errors - ie uncorrectable + message.incrementCorrectedBitCount(2); + + return 2; + } + + /** + * Error detection for CRC-9 protected Confirmed Packet Data blocks. These + * data blocks have a slightly complicated structure because the checksum + * is located between bits 7-15, within a 144 bit block. The checksums + * were generated assuming that the message is contiguous from 0 - 134 bits. + * No data correction is performed. + */ + public static CRC checkCRC9(BinaryMessage message, int messageStart) + { + int calculated = 0x0; //Initial fill of all ones + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(messageStart); + i >= messageStart && i < messageStart + 144; + i = message.nextSetBit(i + 1)) + { + /* message bits before the CRC */ + if(i < (messageStart + 7)) + { + calculated ^= CRC9_CHECKSUMS[i - messageStart]; + } + /* message bits after the CRC */ + else if(i > (messageStart + 15)) + { + calculated ^= CRC9_CHECKSUMS[i - messageStart - 9]; + } + } + + int checksum = message.getInt(messageStart + 7, messageStart + 15); + + int residual = calculated ^ checksum; + + if(residual == 0 || residual == 0x1FF) + { + return CRC.PASSED; + } + + return CRC.FAILED_CRC; + } + + /** + * Calculates the value of the message checksum as a long + */ + public static long getLongChecksum(BinaryMessage message, + int crcStart, int crcLength) + { + return message.getLong(crcStart, crcStart + crcLength - 1); + } + + /** + * Calculates the value of the message checksum as an integer + */ + public static int getIntChecksum(BinaryMessage message, + int crcStart, int crcLength) + { + return message.getInt(crcStart, crcStart + crcLength - 1); + } + + /** + * Identifies any single bit error position that matches the checksum error. + */ + public static int getBitError(long checksumError, long[] checksums) + { + for(int x = 0; x < checksums.length; x++) + { + if(checksums[x] == checksumError) + { + return x; + } + } + + return -1; + } + + /** + * Identifies any single bit error position that matches the checksum error. + */ + public static int getBitError(int checksumError, int[] checksums) + { + for(int x = 0; x < checksums.length; x++) + { + if(checksums[x] == checksumError) + { + return x; + } + } + + return -1; + } + + public static int crc8(BinaryMessage bits, int len) { + int crc=0; + final int K = 8; + final boolean[] poly = new boolean[]{true,false,false,false,false,false,true,true,true}; // crc8 poly + boolean[] buf = new boolean[256]; + if (len+K > 256) { + return 0; + } + for (int i=0; i ******************************************************************************/ @@ -23,217 +23,219 @@ import org.slf4j.LoggerFactory; public class CRCUtil { - private final static Logger mLog = LoggerFactory.getLogger( CRCUtil.class ); + private final static Logger mLog = LoggerFactory.getLogger(CRCUtil.class); - public static long[] generate( int messageSize, - int crcSize, - long polynomial, - long initialFill, - boolean includeCRCBitErrors ) - { - long[] crcTable = new long[ messageSize + - ( includeCRCBitErrors ? crcSize : 0 ) ]; - - int[] checksumIndexes = new int[ crcSize ]; - - for( int x = 0; x < crcSize; x++ ) - { - checksumIndexes[ x ] = messageSize + x; - } + public static long[] generate(int messageSize, + int crcSize, + long polynomial, + long initialFill, + boolean includeCRCBitErrors) + { + long[] crcTable = new long[messageSize + + (includeCRCBitErrors ? crcSize : 0)]; - for( int x = 0; x < messageSize; x++ ) - { - BinaryMessage message = new BinaryMessage( messageSize + crcSize ); - - message.load( messageSize, crcSize, initialFill ); - - message.set( x ); - - message = decode( message, 0, messageSize, polynomial, crcSize ); - - long checksum = message.getLong( checksumIndexes ); - - crcTable[ x ] = checksum; - } - - if( includeCRCBitErrors ) - { - for( int x = 0; x < crcSize; x++ ) - { - crcTable[ messageSize + x ] = Long.rotateLeft( 1, x ); - } - } - - return crcTable; - } + int[] checksumIndexes = new int[crcSize]; - /** - * Generates the checksum table and treats the final bit of each check value - * as a parity bit that complies with the request parity argument. - */ - public static long[] generate( int messageSize, - int crcSize, - long polynomial, - long initialFill, - boolean includeCRCBitErrors, - Parity parity ) - { - long[] table = generate( messageSize, crcSize, polynomial, initialFill, - includeCRCBitErrors ); + for(int x = 0; x < crcSize; x++) + { + checksumIndexes[x] = messageSize + x; + } - if( parity == Parity.NONE ) - { - return table; - } - else - { - for( int x = 0; x < table.length; x++ ) - { - table[ x ] <<= 1; - - if( parity( table[ x ] ) != parity ) - { - table[ x ] ^= 0x1; - } - } - - return table; - } - } - - public enum Parity{ EVEN, ODD, NONE }; + for(int x = 0; x < messageSize; x++) + { + BinaryMessage message = new BinaryMessage(messageSize + crcSize); - /* Determines the parity (number of 1 bits) for the value */ - public static Parity parity( long value ) - { - return Long.bitCount( value ) % 2 == 0 ? Parity.EVEN : Parity.ODD; - } - + message.load(messageSize, crcSize, initialFill); - /** - * Performs binary division of the polynomial into the message for bits - * in index 0 to index messageLength - 1, leaving the results in the - * checksum filed following the message. - * - * IN: MMMMMMMMMMMMMMMMCCCCCCCCCCC - * OUT: 0000000000000000RRRRRRRRRRR - * - * M: Message - * C: Checksum - * R: Remainder - * - * @param message - message and crc - * @param polynomial - crc polynomial - must be 1 bit longer than the crc width - * @param messageSize - message length and start of the checksum - * - * @return message with all message bits zeroed out, and the remainder - * placed in the crc field which starts at index messageLength - */ - public static BinaryMessage decode( BinaryMessage message, - int messageStart, - int messageSize, - long polynomial, - int crcSize ) - { - for (int i = message.nextSetBit( messageStart ); - i >= messageStart && i < messageSize; - i = message.nextSetBit( i+1 ) ) - { - BinaryMessage polySet = new BinaryMessage( crcSize + i + 1 ); - - polySet.load( i, crcSize + 1, polynomial ); - - message.xor( polySet ); - } - - return message; - } - - public static void main( String[] args ) - { - mLog.debug( "Starting" ); - -// String raw = "000000001010001000000000100010100000000000000000010000000011001001011100000000000000000011111110001000101001011100101110000101000110011101101001"; -// BitSetBuffer message = BitSetBuffer.load( raw ); -// -// long polynomial = 0x259l; -// -// decode( message, 0, 135, polynomial, 9 ); -// -// mLog.debug( message.toString() ); + message.set(x); - long[] table = CRCUtil.generate( 135, 9, 0x259l, 0x1FF, false, Parity.NONE ); - mLog.debug( toCodeArray( table ) ); - mLog.debug( "Finished" ); - } - - public static String toCodeArray( long[] values ) - { - boolean integerArray = true; + message = decode(message, 0, messageSize, polynomial, crcSize); - /* Determine the correct primitive type: int or long */ - for( long value: values ) - { - if( value > Integer.MAX_VALUE || value < Integer.MIN_VALUE ) - { - integerArray = false; - break; - } - } + long checksum = message.getLong(checksumIndexes); - StringBuilder sb = new StringBuilder(); + crcTable[x] = checksum; + } - if( integerArray ) - { - sb.append( "\npublic static final int[] CHECKSUMS = new int[]\n" ); - } - else - { - sb.append( "\npublic static final long[] CHECKSUMS = new long[]\n" ); - } - sb.append( "{\n" ); + if(includeCRCBitErrors) + { + for(int x = 0; x < crcSize; x++) + { + crcTable[messageSize + x] = Long.rotateLeft(1, x); + } + } - StringBuilder row = new StringBuilder(); - row.append( " " ); - - for( long value: values ) - { - StringBuilder element = new StringBuilder(); - - element.append( "0x" ); - element.append( Long.toHexString( value ).toUpperCase() ); - - if( !integerArray ) - { - element.append( "l" ); - } - - element.append( ", " ); - - if( row.length() + element.length() <= 80 ) - { - row.append( element.toString() ); - } - else - { - sb.append( row.toString() ); - sb.append( "\n" ); - row = new StringBuilder(); - row.append( " " ); - - row.append( element.toString() ); - } - } - - if( row.length() > 4 ) - { - sb.append( row.toString() ); - sb.append( "\n" ); - } - - sb.append( "};\n" ); + return crcTable; + } - return sb.toString(); - } + /** + * Generates the checksum table and treats the final bit of each check value + * as a parity bit that complies with the request parity argument. + */ + public static long[] generate(int messageSize, + int crcSize, + long polynomial, + long initialFill, + boolean includeCRCBitErrors, + Parity parity) + { + long[] table = generate(messageSize, crcSize, polynomial, initialFill, + includeCRCBitErrors); + if(parity == Parity.NONE) + { + return table; + } + else + { + for(int x = 0; x < table.length; x++) + { + table[x] <<= 1; + + if(parity(table[x]) != parity) + { + table[x] ^= 0x1; + } + } + + return table; + } + } + + public enum Parity + {EVEN, ODD, NONE} + + ; + + /* Determines the parity (number of 1 bits) for the value */ + public static Parity parity(long value) + { + return Long.bitCount(value) % 2 == 0 ? Parity.EVEN : Parity.ODD; + } + + + /** + * Performs binary division of the polynomial into the message for bits + * in index 0 to index messageLength - 1, leaving the results in the + * checksum filed following the message. + * + * IN: MMMMMMMMMMMMMMMMCCCCCCCCCCC + * OUT: 0000000000000000RRRRRRRRRRR + * + * M: Message + * C: Checksum + * R: Remainder + * + * @param message - message and crc + * @param polynomial - crc polynomial - must be 1 bit longer than the crc width + * @param messageSize - message length and start of the checksum + * @return message with all message bits zeroed out, and the remainder + * placed in the crc field which starts at index messageLength + */ + public static BinaryMessage decode(BinaryMessage message, + int messageStart, + int messageSize, + long polynomial, + int crcSize) + { + for(int i = message.nextSetBit(messageStart); + i >= messageStart && i < messageSize; + i = message.nextSetBit(i + 1)) + { + BinaryMessage polySet = new BinaryMessage(crcSize + i + 1); + + polySet.load(i, crcSize + 1, polynomial); + + message.xor(polySet); + System.out.println(message.toString()); + } + + return message; + } + + public static String toCodeArray(long[] values) + { + boolean integerArray = true; + + /* Determine the correct primitive type: int or long */ + for(long value : values) + { + if(value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) + { + integerArray = false; + break; + } + } + + StringBuilder sb = new StringBuilder(); + + if(integerArray) + { + sb.append("\npublic static final int[] CHECKSUMS = new int[]\n"); + } + else + { + sb.append("\npublic static final long[] CHECKSUMS = new long[]\n"); + } + sb.append("{\n"); + + StringBuilder row = new StringBuilder(); + row.append(" "); + + for(long value : values) + { + StringBuilder element = new StringBuilder(); + + element.append("0x"); + element.append(Long.toHexString(value).toUpperCase()); + + if(!integerArray) + { + element.append("l"); + } + + element.append(", "); + + if(row.length() + element.length() <= 80) + { + row.append(element.toString()); + } + else + { + sb.append(row.toString()); + sb.append("\n"); + row = new StringBuilder(); + row.append(" "); + + row.append(element.toString()); + } + } + + if(row.length() > 4) + { + sb.append(row.toString()); + sb.append("\n"); + } + + sb.append("};\n"); + + return sb.toString(); + } + + public static void main(String[] args) + { + mLog.debug("Starting"); + + //DMR message +// String raw = "100110010001000000001001000000001111011011010000011001010000000000000000000000001100001000001011"; + String raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101001001111010001110"; + mLog.debug(raw); + raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101000000000000000000"; + BinaryMessage message = BinaryMessage.load(raw); + mLog.debug(message.toString()); + + long polynomial = 0x11021l; + decode(message, 0, 80, polynomial, 16); + mLog.debug(message.toString()); + mLog.debug("Finished"); + } } diff --git a/src/main/java/io/github/dsheirer/edac/Hamming13.java b/src/main/java/io/github/dsheirer/edac/Hamming13.java new file mode 100644 index 00000000..bb2a1f78 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/Hamming13.java @@ -0,0 +1,129 @@ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; + +/** + * Implements Hamming(13,9,3) Error Detection algorithm + */ +public class Hamming13 +{ + //DMR Checksums from generator matrix TS 102 361-1 Table B.14 + private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0XB, 0xC, 0x6, 0x3}; + private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1}; + + /** + * Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message + * starting at the specified offset. + * + * @param message containing a Hamming protected word + * @param offset to the start of the protected word + * @return message index for an error bit or -1 if no errors are detected. + */ + public static int getErrorIndex(BinaryMessage message, int offset) + { + int syndrome = getSyndrome(message, offset); + + if(syndrome > 0) + { + return offset + ERROR_INDEX[syndrome]; + } + + return -1; + } + + /** + * Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message + * at the specified indices. + * + * @param message containing the Hamming protected word + * @param indices to the word + * @return index of error bit or -1 if no errors are detected + */ + public static int getErrorIndex(BinaryMessage message, int[] indices) + { + int syndrome = getSyndrome(message, indices); + + if(syndrome > 0) + { + int errorIndex = ERROR_INDEX[syndrome]; + + if(errorIndex >= 0 && errorIndex < indices.length) + { + return indices[errorIndex]; + } + else + { + return 10000 + errorIndex; + } + } + + return -1; + } + + /** + * Calculates the parity checksum (Parity 8,4,2,1) for data (9 <> 1 ) bits. + * + * @param message containing Hamming(15) protected word + * @param offset to the Hamming protected word + * @return parity value, 0 - 15 + */ + private static int calculateChecksum(BinaryMessage message, int offset) + { + int calculated = 0; //Starting value + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(offset); i >= offset && i < offset + 9; i = message.nextSetBit(i + 1)) + { + calculated ^= CHECKSUMS[i - offset]; + } + + return calculated; + } + + private static int calculateChecksum(BinaryMessage message, int[] indices) + { + int calculated = 0; //Starting value + + for(int x = 0; x < 9; x++) + { + if(message.get(indices[x])) + { + calculated ^= CHECKSUMS[x]; + } + } + + return calculated; + } + + private static int getSyndrome(BinaryMessage message, int[] indices) + { + int calculated = calculateChecksum(message, indices); + + int checksum = 0; + for(int x = 9; x < 13; x++) + { + checksum = Integer.rotateLeft(checksum, 1); + + if(message.get(indices[x])) + { + checksum++; + } + } + + return calculated ^ checksum; + } + + /** + * Calculates the syndrome as the xor of the calculated checksum and the actual checksum. + * + * @param message containing a hamming(15,11,3) protected word + * @param offset to bit 0 of the hamming protected word + * @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error + */ + private static int getSyndrome(BinaryMessage message, int offset) + { + int calculated = calculateChecksum(message, offset); + int checksum = message.getInt(offset + 9, offset + 12); + return (checksum ^ calculated); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/Hamming15.java b/src/main/java/io/github/dsheirer/edac/Hamming15.java index 22c1e6e2..f498bb72 100644 --- a/src/main/java/io/github/dsheirer/edac/Hamming15.java +++ b/src/main/java/io/github/dsheirer/edac/Hamming15.java @@ -2,116 +2,66 @@ package io.github.dsheirer.edac; import io.github.dsheirer.bits.BinaryMessage; +/** + * Implements Hamming(15,11,3) Error Detection algorithm + */ public class Hamming15 { - private static int[] CHECKSUMS = new int[] { 0xF,0xE,0xD,0xC,0xB,0XA,0x9, - 0x7,0x6,0x5,0x3 }; + //DMR Checksums from generator matrix TS 102 361-1 Table B.15 + private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0XA, 0x5, 0xB, 0xC, 0x6, 0x3}; + private static int[] ERROR_INDEX = new int[]{-1, 14, 13, 10, 12, 6, 9, 4, 11, 0, 5, 7, 8, 1, 3, 2}; - /** - * Performs error detection and correction of any single-bit errors. - * - * @param frame - binary frame containing a hamming(15,11,4) protected field - * - * @param startIndex - offset to the first bit of the field - * - * @return - 0 = no errors - * 1 = a single-bit error was corrected - * 2 = two or more errors detected - no corrections made - */ - public static int checkAndCorrect( BinaryMessage frame, int startIndex ) - { - int syndrome = getSyndrome( frame, startIndex ); + /** + * Calculates the bit error index of the Hamming(15,11,3) protected word that is contained in the binary message + * starting at the specified offset. + * + * @param message containing a Hamming protected word + * @param offset to the start of the protected word + * @return message index for an error bit or -1 if no errors are detected. + */ + public static int getErrorIndex(BinaryMessage message, int offset) + { + int syndrome = getSyndrome(message, offset); - switch( syndrome ) - { - case 0: - return 0; - case 1: - frame.flip( startIndex + 14 ); //Parity 1 - return 1; - case 2: - frame.flip( startIndex + 13 ); //Parity 2 - return 1; - case 3: - frame.flip( startIndex + 10 ); //Data 1 - return 1; - case 4: - frame.flip( startIndex + 12 ); //Parity 4 - return 1; - case 5: - frame.flip( startIndex + 9 ); //Data 2 - return 1; - case 6: - frame.flip( startIndex + 8 ); //Data 3 - return 1; - case 7: - frame.flip( startIndex + 7 ); //Data 4 - return 1; - case 8: - frame.flip( startIndex + 11 ); //Parity 8 - return 1; - case 9: - frame.flip( startIndex + 6 ); //Data 5 - return 1; - case 10: - frame.flip( startIndex + 5 ); //Data 6 - return 1; - case 11: - frame.flip( startIndex + 4 ); //Data 7 - return 1; - case 12: - frame.flip( startIndex + 3 ); //Data 8 - return 1; - case 13: - frame.flip( startIndex + 2 ); //Data 9 - return 1; - case 14: - frame.flip( startIndex + 1 ); //Data 10 - return 1; - case 15: - frame.flip( startIndex ); //Data 11 - return 1; - } + if(syndrome > 0) + { + return offset + ERROR_INDEX[syndrome]; + } - /* We'll never get to here */ - return 2; - } + return -1; + } - /** - * Calculates the checksum (Parity 8,4,2,1) for data (11 <> 1 ) bits. - * @param frame - frame containing hamming(15) protected word - * @param startIndex - start bit index of the hamming protected word - * @return parity value, 0 - 15 - */ - private static int calculateChecksum( BinaryMessage frame, int startIndex ) - { - int calculated = 0; //Starting value + /** + * Calculates the parity checksum (Parity 8,4,2,1) for data (11 <> 1 ) bits. + * + * @param message containing Hamming(15) protected word + * @param offset to the Hamming protected word + * @return parity value, 0 - 15 + */ + private static int calculateChecksum(BinaryMessage message, int offset) + { + int calculated = 0; //Starting value - /* Iterate the set bits and XOR running checksum with lookup value */ - for (int i = frame.nextSetBit( startIndex ); - i >= startIndex && i < startIndex + 11; - i = frame.nextSetBit( i+1 ) ) - { - calculated ^= CHECKSUMS[ i - startIndex ]; - } - - return calculated; - } + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = message.nextSetBit(offset); i >= offset && i < offset + 11; i = message.nextSetBit(i + 1)) + { + calculated ^= CHECKSUMS[i - offset]; + } - /** - * Calculates the syndrome - xor of the calculated checksum and the actual - * checksum. - * - * @param frame - binary frame containing a hamming(15,11,4) protected word - * @param startIndex - of bit 0 of the hamming protected word - * @return - 0 (no errors) or 1 (single bit error corrected) - */ - private static int getSyndrome( BinaryMessage frame, int startIndex ) - { - int calculated = calculateChecksum( frame, startIndex ); - - int checksum = frame.getInt( startIndex + 11, startIndex + 14 ); + return calculated; + } - return ( checksum ^ calculated ); - } + /** + * Calculates the syndrome as the xor of the calculated checksum and the actual checksum. + * + * @param message containing a hamming(15,11,3) protected word + * @param offset to bit 0 of the hamming protected word + * @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error + */ + private static int getSyndrome(BinaryMessage message, int offset) + { + int calculated = calculateChecksum(message, offset); + int checksum = message.getInt(offset + 11, offset + 14); + return (checksum ^ calculated); + } } diff --git a/src/main/java/io/github/dsheirer/edac/Hamming16.java b/src/main/java/io/github/dsheirer/edac/Hamming16.java new file mode 100644 index 00000000..951c4539 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/Hamming16.java @@ -0,0 +1,120 @@ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; + +/** + * Hamming(16,11,5) Error detection and correction utility. + */ +public class Hamming16 +{ + private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07}; + + /** + * Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED) + * + * @param frame - binary frame containing a hamming(16,11,5) protected field + * @param startIndex - offset to the first bit of the field + * @return - 0 = no errors + * 1 = a single-bit error was detected and corrected + * 2 = two or more errors detected - no corrections made + */ + public static int checkAndCorrect(BinaryMessage frame, int startIndex) + { + int syndrome = getSyndrome(frame, startIndex); + + switch(syndrome) + { + case 0: + return 0; + case 1: + frame.flip(startIndex + 15); //Parity 1 + return 1; + case 2: + frame.flip(startIndex + 14); //Parity 2 + return 1; + case 3: + frame.flip(startIndex + 10); //Data 1 + return 1; + case 4: + frame.flip(startIndex + 13); //Parity 4 + return 1; + case 5: + frame.flip(startIndex + 9); //Data 2 + return 1; + case 6: + frame.flip(startIndex + 8); //Data 3 + return 1; + case 7: + frame.flip(startIndex + 7); //Data 4 + return 1; + case 8: + frame.flip(startIndex + 12); //Parity 8 + return 1; + case 9: + frame.flip(startIndex + 6); //Data 5 + return 1; + case 10: + frame.flip(startIndex + 5); //Data 6 + return 1; + case 11: + frame.flip(startIndex + 4); //Data 7 + return 1; + case 12: + frame.flip(startIndex + 3); //Data 8 + return 1; + case 13: + frame.flip(startIndex + 2); //Data 9 + return 1; + case 14: + frame.flip(startIndex + 1); //Data 10 + return 1; + case 15: + frame.flip(startIndex); //Data 11 + return 1; + case 16: + frame.flip(startIndex + 11); //Parity 16 + return 1; + default: + return 2; + } + } + + /** + * Calculates the checksum (Parity 16,8,4,2,1) for data (11 <> 1 ) bits. + * + * @param frame - frame containing hamming protected word + * @param startIndex - start bit index of the hamming protected word + * @return parity value, 0 - 16 + */ + private static int calculateChecksum(BinaryMessage frame, int startIndex) + { + int calculated = 0; //Starting value + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = frame.nextSetBit(startIndex); + i >= startIndex && i < startIndex + 11; + i = frame.nextSetBit(i + 1)) + { + calculated ^= CHECKSUMS[i - startIndex]; + } + + return calculated; + } + + /** + * Calculates the syndrome - xor of the calculated checksum and the actual + * checksum. + * + * @param frame - binary frame containing a hamming(16,11,5) protected word + * @param startIndex - of bit 0 of the hamming protected word + * @return - 0 (no errors) or 1 (single bit error corrected) + */ + private static int getSyndrome(BinaryMessage frame, int startIndex) + { + int calculated = calculateChecksum(frame, startIndex); + + int checksum = frame.getInt(startIndex + 11, startIndex + 15); + + return (checksum ^ calculated); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/Hamming17.java b/src/main/java/io/github/dsheirer/edac/Hamming17.java new file mode 100644 index 00000000..86b17997 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/Hamming17.java @@ -0,0 +1,123 @@ +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.BinaryMessage; + +/** + * Hamming(17,12,3) Error detection and correction utility. + */ +public class Hamming17 +{ + private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05}; + + /** + * Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED) + * + * @param frame - binary frame containing a hamming(17,12,5) protected field + * @param startIndex - offset to the first bit of the field + * @return - 0 = no errors + * 1 = a single-bit error was detected and corrected + * 2 = two or more errors detected - no corrections made + */ + public static int checkAndCorrect(BinaryMessage frame, int startIndex) + { + int syndrome = getSyndrome(frame, startIndex); + + switch(syndrome) + { + case 0: + return 0; + case 1: + frame.flip(startIndex + 16); //Parity 1 + return 1; + case 2: + frame.flip(startIndex + 15); //Parity 2 + return 1; + case 3: + frame.flip(startIndex + 11); //Data 1 + return 1; + case 4: + frame.flip(startIndex + 14); //Parity 4 + return 1; + case 5: + frame.flip(startIndex + 10); //Data 2 + return 1; + case 6: + frame.flip(startIndex + 9); //Data 3 + return 1; + case 7: + frame.flip(startIndex + 8); //Data 4 + return 1; + case 8: + frame.flip(startIndex + 13); //Parity 8 + return 1; + case 9: + frame.flip(startIndex + 7); //Data 5 + return 1; + case 10: + frame.flip(startIndex + 6); //Data 6 + return 1; + case 11: + frame.flip(startIndex + 5); //Data 7 + return 1; + case 12: + frame.flip(startIndex + 4); //Data 8 + return 1; + case 13: + frame.flip(startIndex + 3); //Data 9 + return 1; + case 14: + frame.flip(startIndex + 2); //Data 10 + return 1; + case 15: + frame.flip(startIndex + 1); //Data 11 + return 1; + case 16: + frame.flip(startIndex + 12); //Parity 16 + return 1; + case 17: + frame.flip(startIndex); //Data 12 + return 1; + default: + return 2; + } + } + + /** + * Calculates the checksum (Parity 16,8,4,2,1) for data (12 <> 1 ) bits. + * + * @param frame - frame containing hamming protected word + * @param startIndex - start bit index of the hamming protected word + * @return parity value, 0 - 16 + */ + private static int calculateChecksum(BinaryMessage frame, int startIndex) + { + int calculated = 0; //Starting value + + /* Iterate the set bits and XOR running checksum with lookup value */ + for(int i = frame.nextSetBit(startIndex); + i >= startIndex && i < startIndex + 12; + i = frame.nextSetBit(i + 1)) + { + calculated ^= CHECKSUMS[i - startIndex]; + } + + return calculated; + } + + /** + * Calculates the syndrome - xor of the calculated checksum and the actual + * checksum. + * + * @param frame - binary frame containing a hamming(17,12,5) protected word + * @param startIndex - of bit 0 of the hamming protected word + * @return - 0 (no errors) or 1 (single bit error corrected) + */ + private static int getSyndrome(BinaryMessage frame, int startIndex) + { + int calculated = calculateChecksum(frame, startIndex); + + int checksum = frame.getInt(startIndex + 12, startIndex + 16); + + return (checksum ^ calculated); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java new file mode 100644 index 00000000..b0caa4f4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java @@ -0,0 +1,109 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Berlecamp-Massey decoder for Reed Solomon RS(12,9,4) protected messages using DMR generator polynomial + */ +public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR +{ + private static final int[] FLC_CODEWORD_0 = {0, 1, 2, 3, 4, 5, 6, 7}; + private static final int[] FLC_CODEWORD_1 = {8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] FLC_CODEWORD_2 = {16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] FLC_CODEWORD_3 = {24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] FLC_CODEWORD_4 = {32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] FLC_CODEWORD_5 = {40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] FLC_CODEWORD_6 = {48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] FLC_CODEWORD_7 = {56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] FLC_CODEWORD_8 = {64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] FLC_PARITY_0 = {72, 73, 74, 75, 76, 77, 78, 79}; + private static final int[] FLC_PARITY_1 = {80, 81, 82, 83, 84, 85, 86, 87}; + private static final int[] FLC_PARITY_2 = {88, 89, 90, 91, 92, 93, 94, 95}; + + /** + * Constructs an instance + */ + public ReedSolomon_12_9_4_DMR() + { + //Note: the RS(12,9,4) is a shortened form of RS(255,252,4) over GF(8) + super(255, 252); + } + + /** + * Attempts to correct the Full Link Control message returning true if there were zero or one correctable errors + * and returning false if there were two or more errors detected. + * + * Note: if one bit error was detected and corrected, the corrected bit count is set to 1 in the message + * + * @param message to be corrected. + * @param crcMask as defined in the TS 102-361-1 Table B.21 + * @return true if the message was corrected. + */ + public boolean correctFullLinkControl(CorrectedBinaryMessage message, int crcMask) + { + int[] input = new int[255]; + int[] output = new int[255]; + + input[0] = message.getInt(FLC_PARITY_2) ^ crcMask; + input[1] = message.getInt(FLC_PARITY_1) ^ crcMask; + input[2] = message.getInt(FLC_PARITY_0) ^ crcMask; + input[3] = message.getInt(FLC_CODEWORD_8); + input[4] = message.getInt(FLC_CODEWORD_7); + input[5] = message.getInt(FLC_CODEWORD_6); + input[6] = message.getInt(FLC_CODEWORD_5); + input[7] = message.getInt(FLC_CODEWORD_4); + input[8] = message.getInt(FLC_CODEWORD_3); + input[9] = message.getInt(FLC_CODEWORD_2); + input[10] = message.getInt(FLC_CODEWORD_1); + input[11] = message.getInt(FLC_CODEWORD_0); + + try + { + boolean irrecoverableErrors = decode(input, output); + + if(irrecoverableErrors) + { + return false; + } + } + catch(Exception e) + { + return false; + } + + int pointer = 0; + + for(int x = 11; x >= 0; x--) + { + if(output[x] != -1 && (output[x] != input[x])) + { + message.load(pointer, 8, output[x]); + message.setCorrectedBitCount(1); + return true; + } + + pointer += 8; + } + + return true; + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java similarity index 64% rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java index ad2189a8..291ac0a3 100644 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java @@ -20,21 +20,18 @@ package io.github.dsheirer.edac; -public class ReedSolomon_63_47_17 extends BerlekempMassey_63 +/** + * Reed Solomon RS(24,12,13) decoder over GF(6) + */ +public class ReedSolomon_24_12_13_P25 extends ReedSolomon_63_P25 { /** - * Reed-Solomon RS(63,47,17) decoder. This can also be used for error detection - * and correction of the following RS codes: + * Constructs an instance. * - * RS(36,20,17) - max 8 errors - * RS(24,16,9) - max 4 errors - * RS(24,12,13) - max 6 errors - * - * The maximum correctable errors is determined by (n-k)/2, or hamming - * distance divided by 2. + * Note: this is a shortened form of RS(63,51,13) */ - public ReedSolomon_63_47_17(int maximumCorrectableErrors) + public ReedSolomon_24_12_13_P25() { - super(maximumCorrectableErrors); + super(63,51); } } diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java new file mode 100644 index 00000000..9c05281a --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java @@ -0,0 +1,37 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.edac; + +/** + * Reed Solomon RS(24,16,9) decoder over GF(6) + */ +public class ReedSolomon_24_16_9_P25 extends ReedSolomon_63_P25 +{ + /** + * Constructs an instance. + * + * Note: this is a shortened form of RS(63,55,9) + */ + public ReedSolomon_24_16_9_P25() + { + super(63,55); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java new file mode 100644 index 00000000..2f2e05c9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java @@ -0,0 +1,44 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.edac; + +/** + * Berlekemp Massey decoder for GF(8) using DMR Generator Polynomial + */ +public class ReedSolomon_255_DMR extends BerlekempMassey +{ + /** + * DMR Reed-Solomon code is generated from a Galois Field(2^8) by the polynomial: a8 + a4 + a3 + a2 + a0. + * In binary, this is expressed as: 100011101 which is reversed to big-endian format for this algorithm + * See: TS 102-361-1 paragraph B.3.6 Reed-Solomon(12,9) + */ + public static final int[] DMR_GENERATOR_POLYNOMIAL = {1, 0, 1, 1, 1, 0, 0, 0, 1}; + + /** + * Constructs an instance of RS(total symbols, message symbols) + * + * @param n total symbols + * @param k message symbols + */ + public ReedSolomon_255_DMR(int n, int k) + { + super(8, n, k, DMR_GENERATOR_POLYNOMIAL); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java similarity index 61% rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java index d0155ad8..0c8119a3 100644 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java @@ -22,21 +22,17 @@ package io.github.dsheirer.edac; -public class ReedSolomon_63_35_29 extends BerlekempMassey_63 +/** + * Reed Solomon Decoder for P25 RS(44,16,29) protected messages. + */ +public class ReedSolomon_44_16_29_P25 extends ReedSolomon_63_P25 { /** - * Reed-Solomon RS(63,35,29) decoder. This can also be used for error detection and correction of the following - * RS codes: - * - * RS(46,26,21) - max 10 errors = P25P2 IEMI - * RS(45,26,20) - max 9 errors = P25P2 SOEMI (FACCH) - * RS(52,30,23) - max 11 errors = P25P2 IOEMI (SACCH) - * RS(44,16,29) - max 14 errors = P25P2 ESS - * - * The maximum correctable errors is determined by (n-k)/2, or hamming distance divided by 2. + * Constructs an instance */ - public ReedSolomon_63_35_29(int maximumCorrectableErrors) + public ReedSolomon_44_16_29_P25() { - super(maximumCorrectableErrors); + //The RS(44,16,29) is shortened from RS(63,35,29) + super(63,35); } } diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java similarity index 78% rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java index 14c4eb01..5d9bae07 100644 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java @@ -22,13 +22,16 @@ package io.github.dsheirer.edac; -public class ReedSolomon_44_16_29 extends BerlekempMassey_63 +/** + * Reed Solomon decoder for APCO-25 RS(63,35,29) over GF(6) + */ +public class ReedSolomon_63_35_29_P25 extends ReedSolomon_63_P25 { - //Hamming distance = 29, so max correctable errors = 29 / 2 = 14 - private static final int MAXIMUM_CORRECTABLE_ERRORS = 14; - - public ReedSolomon_44_16_29() + /** + * Reed-Solomon RS(63,35,29) decoder. + */ + public ReedSolomon_63_35_29_P25() { - super(MAXIMUM_CORRECTABLE_ERRORS); + super(63, 35); } } diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java new file mode 100644 index 00000000..e94e9736 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java @@ -0,0 +1,32 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.edac; + +public class ReedSolomon_63_47_17_P25 extends ReedSolomon_63_P25 +{ + /** + * Reed-Solomon RS(63,47,17) decoder. + */ + public ReedSolomon_63_47_17_P25() + { + super(63,47); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java new file mode 100644 index 00000000..38f61142 --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java @@ -0,0 +1,44 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.edac; + +/** + * Berlekemp Massey decoder using APCO-25 Generator Polynomial over GF(6) + */ +public class ReedSolomon_63_P25 extends BerlekempMassey +{ + /** + * APCO-25 Reed-Solomon code is generated from a Galois Field(2^6) by the polynomial: a6 + a1 + a0. + * In binary, this is expressed as: 1000011 which is reversed to big-endian format for this algorithm + * See: TIA 102-BAAA paragraph 5.9 Reed-Solomon Code Generator Matrices + */ + public static final int[] P25_GENERATOR_POLYNOMIAL = { 1, 1, 0, 0, 0, 0, 1 }; + + /** + * Constructs an instance + * + * @param n total symbols + * @param k message symbols + */ + public ReedSolomon_63_P25(int n, int k) + { + super(6, n, k, P25_GENERATOR_POLYNOMIAL); + } +} diff --git a/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java b/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java index b04f90c8..6a797784 100644 --- a/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java +++ b/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java @@ -83,4 +83,10 @@ public class DMR_3_4_Node extends Node { return DMR_TRANSITION_MATRIX[precedingNode.getInputValue()][getInputValue()]; } + + @Override + public String toString() + { + return "IN:" + getInputValue() + " OUT:" + getTransmittedOutputValue(); + } } diff --git a/src/main/java/io/github/dsheirer/edac/trellis/Path.java b/src/main/java/io/github/dsheirer/edac/trellis/Path.java index e4b7eb7e..01d3798e 100644 --- a/src/main/java/io/github/dsheirer/edac/trellis/Path.java +++ b/src/main/java/io/github/dsheirer/edac/trellis/Path.java @@ -18,6 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.edac.trellis; +import com.google.common.base.Joiner; + import java.util.ArrayList; import java.util.List; @@ -128,4 +130,10 @@ public class Path { return new Path(this); } + + @Override + public String toString() + { + return "ERR:" + getError() + " " + Joiner.on(",").join(mNodes); + } } diff --git a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java index 28c87cfd..380cb230 100644 --- a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java +++ b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java @@ -18,21 +18,77 @@ ******************************************************************************/ package io.github.dsheirer.edac.trellis; +import com.google.common.base.Joiner; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +/** + * Viterbi decoder for 3/4 rate trellis coded modulation (TCM) data blocks + */ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder { + private static final int[] DEINTERLEAVE_DIBITS = new int[]{0, 1, 8, 9, 16, 17, 24, 25, 32, 33, 40, 41, 48, 49, 56, 57, 64, 65, 72, 73, 80, 81, + 88, 89, 96, 97, 2, 3, 10, 11, 18, 19, 26, 27, 34, 35, 42, 43, 50, 51, 58, 59, 66, 67, 74, 75, 82, 83, 90, 91, 4, 5, 12, 13, 20, 21, 28, 29, 36, 37, 44, 45, + 52, 53, 60, 61, 68, 69, 76, 77, 84, 85, 92, 93, 6, 7, 14, 15, 22, 23, 30, 31, 38, 39, 46, 47, 54, 55, 62, 63, 70, 71, 78, 79, 86, 87, 94, 95}; + + private static final int[] DEINTERLEAVE_INDEXES = new int[]{0, 1, 2, 3, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50, + 51, 64, 65, 66, 67, 80, 81, 82, 83, 96, 97, 98, 99, 112, 113, 114, 115, 128, 129, 130, 131, 144, 145, 146, 147, + 160, 161, 162, 163, 176, 177, 178, 179, 192, 193, 194, 195, 4, 5, 6, 7, 20, 21, 22, 23, 36, 37, 38, 39, 52, 53, + 54, 55, 68, 69, 70, 71, 84, 85, 86, 87, 100, 101, 102, 103, 116, 117, 118, 119, 132, 133, 134, 135, 148, 149, + 150, 151, 164, 165, 166, 167, 180, 181, 182, 183, 8, 9, 10, 11, 24, 25, 26, 27, 40, 41, 42, 43, 56, 57, 58, 59, + 72, 73, 74, 75, 88, 89, 90, 91, 104, 105, 106, 107, 120, 121, 122, 123, 136, 137, 138, 139, 152, 153, 154, 155, + 168, 169, 170, 171, 184, 185, 186, 187, 12, 13, 14, 15, 28, 29, 30, 31, 44, 45, 46, 47, 60, 61, 62, 63, 76, 77, + 78, 79, 92, 93, 94, 95, 108, 109, 110, 111, 124, 125, 126, 127, 140, 141, 142, 143, 156, 157, 158, 159, 172, + 173, 174, 175, 188, 189, 190, 191}; + /** * Viterbi decoder for Digital Mobile Radio (DMR) 3/4 rate Trellis Coded Modulation (TCM) encoded messages. */ public ViterbiDecoder_3_4_DMR() { - super(3,4); + super(3, 4); } + public static void main(String[] args) + { + List indexes = new ArrayList<>(); + for(int i : DEINTERLEAVE_DIBITS) + { + indexes.add(i * 2); + indexes.add(i * 2 + 1); + } + + System.out.println(Joiner.on(",").join(indexes)); + + Collections.sort(indexes); + System.out.println(Joiner.on(",").join(indexes)); + } + + /** + * Performs deinterleave of transmitted 3/4 TCM encoded payload + * @param interleaved + * @return + */ + private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage interleaved) + { + CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196); + + for(int x = 0; x < 196; x++) + { + if(interleaved.get(x)) + { + deinterleaved.set(DEINTERLEAVE_INDEXES[x]); + } + } + + return deinterleaved; + } + + /** * Decodes a 3/4 rate trellis coded modulation (TCM) encoded DMR binary message containing 196 bits that have * already been deinterleaved. @@ -40,12 +96,11 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder * @param encodedMessage to decode that has already been deinterleaved. * @return decoded message */ - public CorrectedBinaryMessage decode(BinaryMessage encodedMessage) + public CorrectedBinaryMessage decode(CorrectedBinaryMessage encodedMessage) { - int[] symbols = getSymbols(encodedMessage); - + CorrectedBinaryMessage deinterleaved = deinterleave(encodedMessage); + int[] symbols = getSymbols(deinterleaved); Path mostLikelyPath = decode(symbols); - return getMessage(mostLikelyPath); } @@ -104,7 +159,7 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder //Ensure we have an integral number of transmitted symbols (nibbles) in the message if(encodedMessage.size() % getOutputBitLength() != 0) { - throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" + + throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" + " message size: " + encodedMessage.size()); } @@ -125,6 +180,7 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder /** * Creates a DMR node + * * @param inputValue for the specific trellis node * @param transmittedOutputValue the actual transmitted output value * @return created node @@ -154,6 +210,6 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder @Override protected Node createStartingNode() { - return new DMR_3_4_Node(0,0); + return new DMR_3_4_Node(0, 0); } } diff --git a/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java b/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java index ea34d7f7..f9d67644 100644 --- a/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java +++ b/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java @@ -27,10 +27,11 @@ import com.google.common.eventbus.EventBus; */ public class MyEventBus { - private static final EventBus EVENT_BUS = new EventBus(); + private static final EventBus GLOBAL_EVENT_BUS = new EventBus(); - public static EventBus getEventBus() + //TODO: this is primarily used by the Preference Service ... move this event bus to the Preference Service + public static EventBus getGlobalEventBus() { - return EVENT_BUS; + return GLOBAL_EVENT_BUS; } } diff --git a/src/main/java/io/github/dsheirer/filter/FilterSet.java b/src/main/java/io/github/dsheirer/filter/FilterSet.java index 6475778a..e5ecbd9b 100644 --- a/src/main/java/io/github/dsheirer/filter/FilterSet.java +++ b/src/main/java/io/github/dsheirer/filter/FilterSet.java @@ -18,6 +18,11 @@ public class FilterSet implements IFilter { mName = "Message Filter"; } + + public FilterSet(IFilter filter) + { + addFilter(filter); + } public String getName() { diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java index a2b16902..accf5a29 100644 --- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java +++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java @@ -120,7 +120,7 @@ public class JavaFxWindowManager extends Application private void setup() { //Register this class to receive events via each method annotated with @Subscribe - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); //Register JavaFX icon fonts IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont()); @@ -162,6 +162,8 @@ public class JavaFxWindowManager extends Application */ public void shutdown() { + MyEventBus.getGlobalEventBus().unregister(this); + if(mChannelMapStage != null) { mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage); diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 030d0d04..b2691b74 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -337,15 +337,15 @@ public class SDRTrunk implements Listener JMenu viewMenu = new JMenu("View"); JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor"); - viewPlaylistItem.addActionListener(e -> MyEventBus.getEventBus().post(new ViewPlaylistRequest())); + viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest())); viewMenu.add(viewPlaylistItem); JMenuItem preferencesItem = new JMenuItem("User Preferences"); - preferencesItem.addActionListener(e -> MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest())); + preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest())); viewMenu.add(preferencesItem); JMenuItem settingsMenu = new JMenuItem("Icon Manager"); - settingsMenu.addActionListener(arg0 -> MyEventBus.getEventBus().post(new ViewIconManagerRequest())); + settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); viewMenu.add(settingsMenu); JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings"); diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java new file mode 100644 index 00000000..53338a3a --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java @@ -0,0 +1,201 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ +package io.github.dsheirer.gui.instrument.decoder; + +import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart; +import io.github.dsheirer.gui.instrument.chart.DoubleLineChart; +import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart; +import io.github.dsheirer.gui.instrument.chart.PhaseLineChart; +import io.github.dsheirer.gui.instrument.chart.SymbolChart; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.dmr.DMRDecoderInstrumented; +import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DMRPane extends ComplexDecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRPane.class); + + private HBox mSampleChartBox; + private ComplexSampleLineChart mSampleLineChart; + private EyeDiagramChart mEyeDiagramChart; + private HBox mDecoderChartBox; + private SymbolChart mSymbolChart; + private PhaseLineChart mPLLPhaseErrorLineChart; + private DoubleLineChart mPLLFrequencyLineChart; + private DoubleLineChart mSamplesPerSymbolLineChart; + private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster(); + private DMRDecoderInstrumented mDecoder = new DMRDecoderInstrumented(new DecodeConfigDMR()); + + public DMRPane() + { + super(DecoderType.P25_PHASE1); + init(); + } + + private void init() + { + addListener(getDecoder()); + + getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); + getDecoder().setComplexSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + mFilteredBufferBroadcaster.addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); + getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); + + mDecoder.setMessageListener(new Listener() + { + @Override + public void receive(IMessage message) + { + mLog.debug(message.toString()); + } + }); + + } + + @Override + public void setSampleRate(double sampleRate) + { + mLog.debug("Configuring for sample rate: " + sampleRate); + + mDecoder.setSampleRate(sampleRate); + double samplesPerSymbol = sampleRate / 4800.0; + + getSampleLineChart().setSamplesPerSymbol((int) samplesPerSymbol); + } + + private DMRDecoderInstrumented getDecoder() + { + return mDecoder; + } + + private SymbolChart getSymbolChart() + { + if(mSymbolChart == null) + { + mSymbolChart = new SymbolChart(10); + } + + return mSymbolChart; + } + + private HBox getDecoderChartBox() + { + if(mDecoderChartBox == null) + { + mDecoderChartBox = new HBox(); + mDecoderChartBox.setMaxHeight(Double.MAX_VALUE); + getSymbolChart().setMaxWidth(Double.MAX_VALUE); + getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), + getPLLFrequencyLineChart()); + } + + return mDecoderChartBox; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); + getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); + HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(), + getSamplesPerSymbolLineChart()); + } + + return mSampleChartBox; + } + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart("Raw Samples", 100, 10); + } + + return mSampleLineChart; + } + + private EyeDiagramChart getEyeDiagramChart() + { + if(mEyeDiagramChart == null) + { + mEyeDiagramChart = new EyeDiagramChart(10, "Symbol: 3-4"); + } + + return mEyeDiagramChart; + } + + private PhaseLineChart getPLLPhaseErrorLineChart() + { + if(mPLLPhaseErrorLineChart == null) + { + mPLLPhaseErrorLineChart = new PhaseLineChart(40); + } + + return mPLLPhaseErrorLineChart; + } + + private DoubleLineChart getPLLFrequencyLineChart() + { + if(mPLLFrequencyLineChart == null) + { + mPLLFrequencyLineChart = new DoubleLineChart("PLL Frequency", -500, 500, 50, 40); + } + + return mPLLFrequencyLineChart; + } + + private DoubleLineChart getSamplesPerSymbolLineChart() + { + if(mSamplesPerSymbolLineChart == null) + { + mSamplesPerSymbolLineChart = new DoubleLineChart("Sample Point", 9.5, 11.5, 0.1, 40); + } + + return mSamplesPerSymbolLineChart; + } +} diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java index 5702f4da..15d4a195 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java @@ -34,7 +34,8 @@ public class DecoderPaneFactory DecoderType.LTR_NET, DecoderType.MDC1200, DecoderType.MPT1327, - DecoderType.TAIT_1200); + DecoderType.TAIT_1200, + DecoderType.DMR); /** * Creates a decoder pane for the decoder type @@ -59,6 +60,8 @@ public class DecoderPaneFactory throw new IllegalArgumentException("Use the getP25P1DecoderPane() method for P25 decoder type"); case P25_PHASE2: return new P25Phase2HDQPSKPane(); + case DMR: + return new DMRPane(); } return getDefaultPane(); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java index 67b501c6..cd93cd62 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java @@ -151,12 +151,12 @@ public class PlaylistEditor extends BorderPane MenuItem iconManagerItem = new MenuItem("_Icon Manager"); iconManagerItem.setAccelerator(new KeyCodeCombination(KeyCode.I, KeyCombination.ALT_ANY)); - iconManagerItem.setOnAction(event -> MyEventBus.getEventBus().post(new ViewIconManagerRequest())); + iconManagerItem.setOnAction(event -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); viewMenu.getItems().add(iconManagerItem); MenuItem userPreferenceItem = new MenuItem("_User Preferences"); userPreferenceItem.setAccelerator(new KeyCodeCombination(KeyCode.U, KeyCombination.ALT_ANY)); - userPreferenceItem.setOnAction(event -> MyEventBus.getEventBus() + userPreferenceItem.setOnAction(event -> MyEventBus.getGlobalEventBus() .post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.TALKGROUP_FORMAT))); viewMenu.getItems().add(userPreferenceItem); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java index ac4c855d..82b9cc74 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java @@ -172,7 +172,7 @@ public class AliasItemEditor extends Editor mPlaylistManager.getBroadcastModel().getConfiguredBroadcasts() .addListener((ListChangeListener)c -> updateStreamViews()); - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); setMaxWidth(Double.MAX_VALUE); @@ -410,6 +410,7 @@ public class AliasItemEditor extends Editor @Override public void dispose() { + MyEventBus.getGlobalEventBus().unregister(this); } private VBox getTitledPanesBox() @@ -659,7 +660,7 @@ public class AliasItemEditor extends Editor if(selected != null) { - MyEventBus.getEventBus().post(new ViewAliasIdentifierRequest(selected)); + MyEventBus.getGlobalEventBus().post(new ViewAliasIdentifierRequest(selected)); } } }); @@ -687,6 +688,12 @@ public class AliasItemEditor extends Editor p25Menu.getItems().add(new SeparatorMenuItem()); p25Menu.getItems().add(new AddTonesItem("Audio Tones (Phase 2 Only)")); + Menu dmrMenu = new ProtocolMenu(Protocol.DMR); + dmrMenu.getItems().add(new AddTalkgroupItem(Protocol.DMR)); + dmrMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.DMR)); + dmrMenu.getItems().add(new AddRadioIdItem(Protocol.DMR)); + dmrMenu.getItems().add(new AddRadioIdRangeItem(Protocol.DMR)); + Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC); fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC)); fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.FLEETSYNC)); @@ -715,8 +722,8 @@ public class AliasItemEditor extends Editor Menu lojackMenu = new ProtocolMenu(Protocol.LOJACK); lojackMenu.getItems().add(new AddLojackItem()); - mAddIdentifierButton.getItems().addAll(p25Menu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, passportMenu, - taitMenu, new SeparatorMenuItem(), lojackMenu); + mAddIdentifierButton.getItems().addAll(p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, + passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu); } return mAddIdentifierButton; diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java index 8be9febc..6124aea7 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java @@ -178,7 +178,7 @@ public class AliasViewByIdentifierEditor extends VBox if(selected != null) { - MyEventBus.getEventBus().post(new ViewAliasRequest(selected.getAlias())); + MyEventBus.getGlobalEventBus().post(new ViewAliasRequest(selected.getAlias())); } }); } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java index fd3d8ddd..96503008 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java @@ -237,6 +237,10 @@ public class RadioIdEditor extends IdentifierEditor "Format: 0 - 16777215")); mRadioDetails.add(new RadioDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF")); + mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF), + "Format: 0 - 16777215")); + mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), + "Format: 0 - FFFFFF")); mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607")); mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java index 25fb0e95..a1045631 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java @@ -239,6 +239,10 @@ public class TalkgroupEditor extends IdentifierEditor "Format: 0 - 65535")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), "Format: 0 - FFFF")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF), + "Format: 0 - 16,777,215")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), + "Format: 0 - FFFFFF")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED, new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.LTR, IntegerFormat.FORMATTED, new LtrFormatter(0,0x3FFF), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java index 74290349..2744cf88 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java @@ -358,7 +358,7 @@ public abstract class ChannelConfigurationEditor extends Editor if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES) { - MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY)); + MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY)); return; } } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java index 8f7a733d..e0924b65 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java @@ -52,6 +52,8 @@ public class ChannelConfigurationEditorFactory { case AM: return new AMConfigurationEditor(playlistManager, userPreferences); + case DMR: + return new DMRConfigurationEditor(playlistManager, userPreferences); case NBFM: return new NBFMConfigurationEditor(playlistManager, userPreferences); case LTR_NET: diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java new file mode 100644 index 00000000..bd91d8e6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java @@ -0,0 +1,726 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.gui.playlist.channel; + +import io.github.dsheirer.gui.control.IntegerTextField; +import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor; +import io.github.dsheirer.gui.playlist.record.RecordConfigurationEditor; +import io.github.dsheirer.gui.playlist.source.FrequencyEditor; +import io.github.dsheirer.gui.playlist.source.FrequencyField; +import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration; +import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; +import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; +import io.github.dsheirer.module.log.EventLogType; +import io.github.dsheirer.module.log.config.EventLogConfiguration; +import io.github.dsheirer.playlist.PlaylistManager; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.record.RecorderType; +import io.github.dsheirer.record.config.RecordConfiguration; +import io.github.dsheirer.source.config.SourceConfiguration; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.util.Callback; +import org.controlsfx.control.ToggleSwitch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR channel configuration editor + */ +public class DMRConfigurationEditor extends ChannelConfigurationEditor +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRConfigurationEditor.class); + private TitledPane mDecoderPane; + private TitledPane mEventLogPane; + private TitledPane mRecordPane; + private TitledPane mSourcePane; + private SourceConfigurationEditor mSourceConfigurationEditor; + private EventLogConfigurationEditor mEventLogConfigurationEditor; + private RecordConfigurationEditor mRecordConfigurationEditor; + private ToggleSwitch mIgnoreDataCallsButton; + private Spinner mTrafficChannelPoolSizeSpinner; + private TableView mTimeslotFrequencyTable; + private IntegerTextField mLogicalSlotNumberField; + private FrequencyField mDownlinkFrequencyField; +// private FrequencyField mUplinkFrequencyField; + private Button mAddTimeslotFrequencyButton; + private Button mDeleteTimeslotFrequencyButton; + private Spinner mChannelRotationDelaySpinner; + + /** + * Constructs an instance + * @param playlistManager + */ + public DMRConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences) + { + super(playlistManager, userPreferences); + getTitledPanesBox().getChildren().add(getSourcePane()); + getTitledPanesBox().getChildren().add(getDecoderPane()); + getTitledPanesBox().getChildren().add(getEventLogPane()); + getTitledPanesBox().getChildren().add(getRecordPane()); + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.DMR; + } + + private TitledPane getSourcePane() + { + if(mSourcePane == null) + { + mSourcePane = new TitledPane("Source", getSourceConfigurationEditor()); + mSourcePane.setExpanded(true); + } + + return mSourcePane; + } + + private TitledPane getDecoderPane() + { + if(mDecoderPane == null) + { + mDecoderPane = new TitledPane(); + mDecoderPane.setText("Decoder: DMR"); + mDecoderPane.setExpanded(true); + + GridPane gridPane = new GridPane(); + gridPane.setPadding(new Insets(10,10,10,10)); + gridPane.setHgap(10); + gridPane.setVgap(10); + + int row = 0; + + Label poolSizeLabel = new Label("Max Traffic Channels"); + GridPane.setHalignment(poolSizeLabel, HPos.RIGHT); + GridPane.setConstraints(poolSizeLabel, 0, row); + gridPane.getChildren().add(poolSizeLabel); + + GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row); + gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner()); + + GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row); + gridPane.getChildren().add(getIgnoreDataCallsButton()); + + Label directionLabel = new Label("Ignore Data Calls"); + GridPane.setHalignment(directionLabel, HPos.LEFT); + GridPane.setConstraints(directionLabel, 3, row); + gridPane.getChildren().add(directionLabel); + + Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies"); + GridPane.setHalignment(timeslotTableLabel, HPos.LEFT); + GridPane.setConstraints(timeslotTableLabel, 0, ++row, 4, 1); + gridPane.getChildren().add(timeslotTableLabel); + + GridPane.setConstraints(getTimeslotTable(), 0, ++row, 4, 3); + gridPane.getChildren().add(getTimeslotTable()); + + VBox buttonsBox = new VBox(); + buttonsBox.setAlignment(Pos.CENTER); + buttonsBox.setSpacing(10); + buttonsBox.getChildren().addAll(getAddTimeslotFrequencyButton(), getDeleteTimeslotFrequencyButton()); + + GridPane.setConstraints(buttonsBox, 4, row, 1, 3); + gridPane.getChildren().addAll(buttonsBox); + + row += 3; + + HBox editorBox = new HBox(); + editorBox.setAlignment(Pos.CENTER_LEFT); + editorBox.setSpacing(5); + + Label lsnLabel = new Label("LSN"); + editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField()); + + Label downlinkLabel = new Label("Frequency (MHz)"); + downlinkLabel.setPadding(new Insets(0,0,0,5)); + editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField()); + +// Label uplinkLabel = new Label("Uplink"); +// uplinkLabel.setPadding(new Insets(0,0,0,5)); +// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField()); + + GridPane.setConstraints(editorBox, 0, row, 4, 1); + gridPane.getChildren().add(editorBox); + + mDecoderPane.setContent(gridPane); + } + + return mDecoderPane; + } + + private TitledPane getEventLogPane() + { + if(mEventLogPane == null) + { + mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor()); + mEventLogPane.setExpanded(false); + } + + return mEventLogPane; + } + + private TitledPane getRecordPane() + { + if(mRecordPane == null) + { + mRecordPane = new TitledPane(); + mRecordPane.setText("Recording"); + mRecordPane.setExpanded(false); + + Label notice = new Label("Note: use aliases to control call audio recording"); + notice.setPadding(new Insets(10, 10, 0, 10)); + + VBox vBox = new VBox(); + vBox.getChildren().addAll(getRecordConfigurationEditor(), notice); + + mRecordPane.setContent(vBox); + } + + return mRecordPane; + } + + private SourceConfigurationEditor getSourceConfigurationEditor() + { + if(mSourceConfigurationEditor == null) + { + mSourceConfigurationEditor = new FrequencyEditor(getTunerModel(), true); + + //Add a listener so that we can push change notifications up to this editor + mSourceConfigurationEditor.modifiedProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mSourceConfigurationEditor; + } + + private EventLogConfigurationEditor getEventLogConfigurationEditor() + { + if(mEventLogConfigurationEditor == null) + { + List types = new ArrayList<>(); + types.add(EventLogType.CALL_EVENT); + types.add(EventLogType.DECODED_MESSAGE); + types.add(EventLogType.TRAFFIC_CALL_EVENT); + types.add(EventLogType.TRAFFIC_DECODED_MESSAGE); + + mEventLogConfigurationEditor = new EventLogConfigurationEditor(types); + mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5)); + mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mEventLogConfigurationEditor; + } + + private TableView getTimeslotTable() + { + if(mTimeslotFrequencyTable == null) + { + mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor())); + mTimeslotFrequencyTable.setPrefHeight(100.0); + + TableColumn numberColumn = new TableColumn("LSN"); + numberColumn.setPrefWidth(70); + numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty()); + mTimeslotFrequencyTable.getColumns().addAll(numberColumn); + mTimeslotFrequencyTable.getSortOrder().add(numberColumn); + + TableColumn downlinkColumn = new TableColumn("Frequency (MHz)"); + downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz()); + downlinkColumn.setPrefWidth(175); + mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn); + +// TableColumn uplinkColumn = new TableColumn("Uplink (MHz)"); +// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz()); +// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn); + + mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> setTimeslot(newValue)); + } + + return mTimeslotFrequencyTable; + } + + /** + * Sets the specified timeslot frequency into the editor + */ + private void setTimeslot(TimeslotFrequency timeslot) + { + //Preserve the current modified flag state since setting values in the editor will change it. + boolean modified = modifiedProperty().get(); + + getLogicalSlotNumberField().setDisable(timeslot == null); + getDownlinkFrequencyField().setDisable(timeslot == null); +// getUplinkFrequencyField().setDisable(timeslot == null); + getDeleteTimeslotFrequencyButton().setDisable(timeslot == null); + + if(timeslot != null) + { + getLogicalSlotNumberField().set(timeslot.getNumber()); + getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency()); +// getUplinkFrequencyField().set(timeslot.getUplinkFrequency()); + } + else + { + getLogicalSlotNumberField().set(0); + getDownlinkFrequencyField().set(0); +// getUplinkFrequencyField().set(0); + } + + modifiedProperty().set(modified); + } + + private Button getAddTimeslotFrequencyButton() + { + if(mAddTimeslotFrequencyButton == null) + { + mAddTimeslotFrequencyButton = new Button("Add"); + mAddTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE); + mAddTimeslotFrequencyButton.setOnAction(event -> addTimeslot()); + } + + return mAddTimeslotFrequencyButton; + } + + /** + * Adds a new timeslot frequency value and makes a best guess of the next sequential LSN number + */ + private void addTimeslot() + { + int lsn = 1; + + while(hasLSN(lsn) && lsn <= 64) //64 is an arbitrary value to keep it from going too high + { + lsn++; + } + + TimeslotFrequency timeslotFrequency = new TimeslotFrequency(); + timeslotFrequency.setNumber(lsn); + getTimeslotTable().getItems().add(timeslotFrequency); + getTimeslotTable().scrollTo(timeslotFrequency); + getTimeslotTable().getSelectionModel().select(timeslotFrequency); + modifiedProperty().set(true); + } + + /** + * Searches the current timeslot frequency list to determine if the specified lsn is already listed + */ + private boolean hasLSN(int lsn) + { + for(TimeslotFrequency timeslotFrequency: getTimeslotTable().getItems()) + { + if(timeslotFrequency.getNumber() == lsn) + { + return true; + } + } + + return false; + } + + private Button getDeleteTimeslotFrequencyButton() + { + if(mDeleteTimeslotFrequencyButton == null) + { + mDeleteTimeslotFrequencyButton = new Button("Delete"); + mDeleteTimeslotFrequencyButton.setDisable(true); + mDeleteTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE); + mDeleteTimeslotFrequencyButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + getTimeslotTable().getItems().remove(selected); + modifiedProperty().set(true); + } + } + }); + } + + return mDeleteTimeslotFrequencyButton; + } + + private IntegerTextField getLogicalSlotNumberField() + { + if(mLogicalSlotNumberField == null) + { + mLogicalSlotNumberField = new IntegerTextField(); + mLogicalSlotNumberField.setDisable(true); + mLogicalSlotNumberField.setPrefWidth(65); + mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> { + TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + Integer value = mLogicalSlotNumberField.get(); + + if(value != null) + { + selected.setNumber(value); + } + } + + modifiedProperty().set(true); + }); + } + + return mLogicalSlotNumberField; + } + + private FrequencyField getDownlinkFrequencyField() + { + if(mDownlinkFrequencyField == null) + { + mDownlinkFrequencyField = new FrequencyField(); + mDownlinkFrequencyField.setDisable(true); + mDownlinkFrequencyField.textProperty().addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, String oldValue, String newValue) + { + TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + selected.setDownlinkFrequency(mDownlinkFrequencyField.get()); + } + + modifiedProperty().set(true); + } + }); + } + + return mDownlinkFrequencyField; + } + +// private FrequencyField getUplinkFrequencyField() +// { +// if(mUplinkFrequencyField == null) +// { +// mUplinkFrequencyField = new FrequencyField(); +// mUplinkFrequencyField.setDisable(true); +// mUplinkFrequencyField.textProperty().addListener(new ChangeListener() +// { +// @Override +// public void changed(ObservableValue observable, String oldValue, String newValue) +// { +// TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem(); +// +// if(selected != null) +// { +// selected.setUplinkFrequency(mUplinkFrequencyField.get()); +// int lsn = selected.getNumber(); +// selected.setNumber(-1); +// selected.setNumber(lsn); +// } +// +// modifiedProperty().set(true); +// } +// }); +// } +// +// return mUplinkFrequencyField; +// } + + private ToggleSwitch getIgnoreDataCallsButton() + { + if(mIgnoreDataCallsButton == null) + { + mIgnoreDataCallsButton = new ToggleSwitch(); + mIgnoreDataCallsButton.setDisable(true); + mIgnoreDataCallsButton.selectedProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mIgnoreDataCallsButton; + } + + private Spinner getTrafficChannelPoolSizeSpinner() + { + if(mTrafficChannelPoolSizeSpinner == null) + { + mTrafficChannelPoolSizeSpinner = new Spinner(); + mTrafficChannelPoolSizeSpinner.setDisable(true); + mTrafficChannelPoolSizeSpinner.setTooltip( + new Tooltip("Maximum number of traffic channels that can be created by the decoder")); + mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL); + SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50); + mTrafficChannelPoolSizeSpinner.setValueFactory(svf); + mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mTrafficChannelPoolSizeSpinner; + } + + /** + * Channel rotation monitor delay value. This dictates how long the decoder will remain on each frequency before + * rotating to the next frequency in the list + * @return spinner + */ + private Spinner getChannelRotationDelaySpinner() + { + if(mChannelRotationDelaySpinner == null) + { + mChannelRotationDelaySpinner = new Spinner(); + mChannelRotationDelaySpinner.setDisable(true); + mChannelRotationDelaySpinner.setTooltip( + new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency")); + mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL); + SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(200, 2000, 200, 50); + mChannelRotationDelaySpinner.setValueFactory(svf); + mChannelRotationDelaySpinner.getValueFactory().valueProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mChannelRotationDelaySpinner; + } + + private RecordConfigurationEditor getRecordConfigurationEditor() + { + if(mRecordConfigurationEditor == null) + { + List types = new ArrayList<>(); + types.add(RecorderType.BASEBAND); + types.add(RecorderType.DEMODULATED_BIT_STREAM); + types.add(RecorderType.MBE_CALL_SEQUENCE); + types.add(RecorderType.TRAFFIC_BASEBAND); + types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM); + types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE); + mRecordConfigurationEditor = new RecordConfigurationEditor(types); + mRecordConfigurationEditor.setDisable(true); + mRecordConfigurationEditor.modifiedProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mRecordConfigurationEditor; + } + + @Override + protected void setDecoderConfiguration(DecodeConfiguration config) + { + getIgnoreDataCallsButton().setDisable(config == null); + getTrafficChannelPoolSizeSpinner().setDisable(config == null); + getTimeslotTable().getItems().clear(); + getTimeslotTable().setDisable(config == null); + getAddTimeslotFrequencyButton().setDisable(config == null); + getDeleteTimeslotFrequencyButton().setDisable(true); + getLogicalSlotNumberField().set(0); + getLogicalSlotNumberField().setDisable(true); + getDownlinkFrequencyField().set(0); + getDownlinkFrequencyField().setDisable(true); +// getUplinkFrequencyField().set(0); +// getUplinkFrequencyField().setDisable(true); + getChannelRotationDelaySpinner().setDisable(config == null); + + if(config instanceof DecodeConfigDMR) + { + DecodeConfigDMR decodeConfig = (DecodeConfigDMR)config; + + getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls()); + getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize()); + + for(TimeslotFrequency timeslotFrequency: decodeConfig.getTimeslotMap()) + { + getTimeslotTable().getItems().add(timeslotFrequency.copy()); + } + } + else + { + getIgnoreDataCallsButton().setSelected(false); + getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0); + getChannelRotationDelaySpinner().getValueFactory().setValue(200); + } + } + + @Override + protected void saveDecoderConfiguration() + { + DecodeConfigDMR config; + + if(getItem().getDecodeConfiguration() instanceof DecodeConfigDMR) + { + config = (DecodeConfigDMR)getItem().getDecodeConfiguration(); + } + else + { + config = new DecodeConfigDMR(); + } + +// config.setChannelRotationDelay(getChannelRotationDelaySpinner().getValue()); + config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected()); + config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue()); + config.setTimeslotMap(new ArrayList<>(getTimeslotTable().getItems())); + getItem().setDecodeConfiguration(config); + } + + @Override + protected void setEventLogConfiguration(EventLogConfiguration config) + { + getEventLogConfigurationEditor().setItem(config); + } + + @Override + protected void saveEventLogConfiguration() + { + getEventLogConfigurationEditor().save(); + + if(getEventLogConfigurationEditor().getItem().getLoggers().isEmpty()) + { + getItem().setEventLogConfiguration(null); + } + else + { + getItem().setEventLogConfiguration(getEventLogConfigurationEditor().getItem()); + } + } + + @Override + protected void setAuxDecoderConfiguration(AuxDecodeConfiguration config) + { + //no-op + } + + @Override + protected void saveAuxDecoderConfiguration() + { + //no-op + } + + @Override + protected void setRecordConfiguration(RecordConfiguration config) + { + getRecordConfigurationEditor().setDisable(config == null); + getRecordConfigurationEditor().setItem(config); + } + + @Override + protected void saveRecordConfiguration() + { + getRecordConfigurationEditor().save(); + RecordConfiguration config = getRecordConfigurationEditor().getItem(); + getItem().setRecordConfiguration(config); + } + + @Override + protected void setSourceConfiguration(SourceConfiguration config) + { + getSourceConfigurationEditor().setSourceConfiguration(config); + } + + @Override + protected void saveSourceConfiguration() + { + getSourceConfigurationEditor().save(); + SourceConfiguration sourceConfiguration = getSourceConfigurationEditor().getSourceConfiguration(); + getItem().setSourceConfiguration(sourceConfiguration); + } + + /** + * Channel tuner channel source frequencies value factory + */ + public class FrequencyCellValueFactory implements Callback, + ObservableValue> + { + private SimpleStringProperty mFrequency = new SimpleStringProperty(); + private boolean mIsDownlink; + + public FrequencyCellValueFactory(boolean isDownlink) + { + mIsDownlink = isDownlink; + } + + @Override + public ObservableValue call(TableColumn.CellDataFeatures param) + { + if(param.getValue() != null) + { + long frequency = (mIsDownlink ? param.getValue().getDownlinkFrequency() : param.getValue().getUplinkFrequency()); + mFrequency.set(String.valueOf(frequency / 1E6)); + } + else + { + mFrequency.set(null); + } + + return mFrequency; + } + } + + public class DownlinkPropertyValueFactory extends PropertyValueFactory + { + private StringProperty mStringProperty = new SimpleStringProperty(); + + public DownlinkPropertyValueFactory() + { + super("downlinkFrequency"); + } + + @Override + public ObservableValue call(TableColumn.CellDataFeatures param) + { + if(param.getValue() != null) + { + mStringProperty.set(String.valueOf(param.getValue().getDownlinkFrequency() / 1E6)); + } + else + { + mStringProperty.setValue(null); + } + + return mStringProperty; + } + } + +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java index 2fbd8a33..800e2fab 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java @@ -276,7 +276,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor channelMapName = getChannelMapComboBox().getSelectionModel().getSelectedItem().getName(); } - MyEventBus.getEventBus().post(new ViewChannelMapEditorRequest(channelMapName)); + MyEventBus.getGlobalEventBus().post(new ViewChannelMapEditorRequest(channelMapName)); } }); } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java index 4fe29864..c7be9524 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java @@ -94,7 +94,7 @@ public class PlaylistManagerEditor extends HBox mUserPreferences = userPreferences; //Register to receive preferences updates - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); setPadding(new Insets(5, 5, 5, 5)); HBox.setHgrow(getPlaylistTableView(), Priority.ALWAYS); @@ -102,6 +102,11 @@ public class PlaylistManagerEditor extends HBox updateButtons(); } + public void dispose() + { + MyEventBus.getGlobalEventBus().unregister(this); + } + /** * Indicates if the path argument is the same as the current playlist path */ diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java index 2ec7bbd6..90b76992 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java @@ -376,7 +376,7 @@ public class FrequencyEditor extends VBox if(getShowCreatedChannelCheckBox().selectedProperty().get()) { - MyEventBus.getEventBus().post(new ViewChannelRequest(channel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel)); } else { diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java index a04335e6..80723b79 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java @@ -31,7 +31,7 @@ public enum ModeDecoderType { AM("am", DecoderType.AM), APCO25("p25", DecoderType.P25_PHASE1), - DMR("dmr", null), + DMR("dmr", DecoderType.DMR), DSTAR("d-star", null), FM("fm", DecoderType.NBFM), FMN("fmn", DecoderType.NBFM), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java index 4894fcb1..6477ea1c 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java @@ -24,6 +24,7 @@ import io.github.dsheirer.identifier.talkgroup.LTRTalkgroup; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.UnknownTalkgroupIdentifier; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup; import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup; @@ -33,7 +34,9 @@ import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormat import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.rrapi.type.Flavor; import io.github.dsheirer.rrapi.type.Site; +import io.github.dsheirer.rrapi.type.SiteFrequency; import io.github.dsheirer.rrapi.type.System; +import io.github.dsheirer.rrapi.type.SystemInformation; import io.github.dsheirer.rrapi.type.Tag; import io.github.dsheirer.rrapi.type.Talkgroup; import io.github.dsheirer.rrapi.type.Type; @@ -42,6 +45,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -228,6 +232,19 @@ public class RadioReferenceDecoder return null; } + /** + * Provides the radio reference Type for the system information instance + */ + public Type getType(SystemInformation systemInformation) + { + if(systemInformation != null) + { + return mTypeMap.get(systemInformation.getTypeId()); + } + + return null; + } + /** * Provides the radio reference Flavor for the system */ @@ -241,6 +258,19 @@ public class RadioReferenceDecoder return null; } + /** + * Provides the radio reference Flavor for the system information instance + */ + public Flavor getFlavor(SystemInformation systemInformation) + { + if(systemInformation != null) + { + return mFlavorMap.get(systemInformation.getFlavorId()); + } + + return null; + } + /** * Provides the radio reference voice/traffic channel protocol for the system */ @@ -302,6 +332,66 @@ public class RadioReferenceDecoder return false; } + /** + * Indicates if the specified site is a DMR Connect Plus site that has site frequencies that can be converted + * to timeslot frequencies + * @param systemInformation to determine the system type + * @param site containing site frequencies + */ + public boolean hasTimeslotFrequencies(SystemInformation systemInformation, Site site) + { + Type type = getType(systemInformation); + Flavor flavor = getFlavor(systemInformation); + return type != null && flavor != null && type.getName().contains("DMR") && !site.getSiteFrequencies().isEmpty(); + } + + /** + * Creates a list of timeslot to frequency mappings from the radio reference site's list of site frequencies for a + * MotoTRBO Connect Plus site + * @param systemInformation to detect the system type (DMR) and flavor (Connect Plus). + * @param site containing 1 or more site frequencies + * @return list of timeslot frequency mappings or an empty list. + */ + public List getTimeslotFrequencies(SystemInformation systemInformation, Site site) + { + if(hasTimeslotFrequencies(systemInformation, site)) + { + List frequencies = new ArrayList<>(); + + for(SiteFrequency siteFrequency: site.getSiteFrequencies()) + { + int lcn = siteFrequency.getLogicalChannelNumber(); + + if(siteFrequency.getChannelId() != null) + { + try + { + lcn = Integer.parseInt(siteFrequency.getChannelId()); + } + catch(Exception e) + { + //Do nothing, we couldn't parse the LSN from the channel ID value + } + } + + int lsn = (lcn - 1) * 2 + 1; + TimeslotFrequency timeslot1Frequency = new TimeslotFrequency(); + timeslot1Frequency.setNumber(lsn); + timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6)); + frequencies.add(timeslot1Frequency); + + TimeslotFrequency timeslot2Frequency = new TimeslotFrequency(); + timeslot2Frequency.setNumber(lsn + 1); + timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6)); + frequencies.add(timeslot2Frequency); + } + + return frequencies; + } + + return Collections.emptyList(); + } + /** * Identifies the sdrtrunk protocol used by the system. * @param system to identify @@ -320,6 +410,8 @@ public class RadioReferenceDecoder switch(type.getName()) { + case "DMR": + return Protocol.DMR; case "LTR": if(flavor.getName().contentEquals("Standard") || flavor.getName().contentEquals("Net")) { @@ -341,7 +433,6 @@ public class RadioReferenceDecoder return Protocol.APCO25; } break; - case "DMR": case "NXDN": case "EDACS": case "TETRA": @@ -371,6 +462,8 @@ public class RadioReferenceDecoder { switch(type.getName()) { + case "DMR": + return DecoderType.DMR; case "LTR": if(flavor.getName().contentEquals("Net")) { @@ -403,7 +496,6 @@ public class RadioReferenceDecoder return DecoderType.P25_PHASE1; } break; - case "DMR": case "NXDN": case "EDACS": diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java index c79f43d7..498d4bc9 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java @@ -27,6 +27,8 @@ import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest; import io.github.dsheirer.module.decode.DecoderFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; +import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder; import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2; @@ -222,6 +224,12 @@ public class SiteEditor extends GridPane switch(decoderType) { + case DMR: + DecodeConfigDMR dmr = new DecodeConfigDMR(); + List timeslotFrequencies = mRadioReferenceDecoder + .getTimeslotFrequencies(systemInformation, site); + dmr.setTimeslotMap(timeslotFrequencies); + return dmr; case P25_PHASE1: DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType); @@ -290,6 +298,7 @@ public class SiteEditor extends GridPane } } + public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder) { mCurrentSite = site; @@ -473,7 +482,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null) { - MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel)); } } @@ -535,7 +544,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected()) { - MyEventBus.getEventBus().post(new ViewChannelRequest(channel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel)); } } } @@ -609,7 +618,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected()) { - MyEventBus.getEventBus().post(new ViewChannelRequest(channel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel)); } } else @@ -637,7 +646,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null) { - MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel)); } } } @@ -701,7 +710,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected()) { - MyEventBus.getEventBus().post(new ViewChannelRequest(channel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel)); } } else @@ -730,7 +739,7 @@ public class SiteEditor extends GridPane if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null) { - MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel)); + MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel)); } } } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java index eff5eb3e..15ecdc6c 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java @@ -105,7 +105,7 @@ public class SystemTalkgroupSelectionEditor extends GridPane public SystemTalkgroupSelectionEditor(UserPreferences userPreferences, PlaylistManager playlistManager) { //Register to receive flash alias box requests - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); mUserPreferences = userPreferences; mPlaylistManager = playlistManager; @@ -172,6 +172,11 @@ public class SystemTalkgroupSelectionEditor extends GridPane getChildren().add(getTalkgroupEditor()); } + public void dispose() + { + MyEventBus.getGlobalEventBus().unregister(this); + } + public void clear() { mTalkgroupList.clear(); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java index 02b48a2d..5f7ec8ef 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java @@ -278,7 +278,7 @@ public class TalkgroupEditor extends GridPane alert.setHeaderText("An alias list is required to create aliases"); alert.initOwner((getCreateAliasButton()).getScene().getWindow()); alert.showAndWait(); - MyEventBus.getEventBus().post(new FlashAliasListComboBoxRequest()); + MyEventBus.getGlobalEventBus().post(new FlashAliasListComboBoxRequest()); } else if(mRadioReferenceDecoder != null && mTalkgroup != null && mSystem != null) { @@ -309,7 +309,7 @@ public class TalkgroupEditor extends GridPane mEditAliasButton.setOnAction(event -> { if(mAlias != null) { - MyEventBus.getEventBus().post(new ViewAliasRequest(mAlias)); + MyEventBus.getGlobalEventBus().post(new ViewAliasRequest(mAlias)); } }); } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java index 92a0a216..dd879f67 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java @@ -26,8 +26,12 @@ import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; import io.github.dsheirer.source.config.SourceConfiguration; import io.github.dsheirer.source.tuner.TunerModel; +import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor; +import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -36,6 +40,9 @@ import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import org.slf4j.Logger; @@ -54,9 +61,10 @@ public class FrequencyEditor extends SourceConfigurationEditor mFrequencyBoxes = new ArrayList<>(); + private ObservableList mFrequencyBoxes = FXCollections.observableArrayList(); private ComboBox mPreferredTunerComboBox; private VBox mFrequencyBoxContainer; + private Spinner mChannelRotationDelaySpinner; private boolean mAllowMultipleFrequencies; /** @@ -80,9 +88,24 @@ public class FrequencyEditor extends SourceConfigurationEditor getFrequencyRotationDelaySpinner() + { + if(mChannelRotationDelaySpinner == null) + { + mChannelRotationDelaySpinner = new Spinner(); + mChannelRotationDelaySpinner.setTooltip( + new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency")); + mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL); + int min = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM; + int max = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MAXIMUM; + SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, min, 50); + mChannelRotationDelaySpinner.setValueFactory(svf); + mChannelRotationDelaySpinner.getValueFactory().valueProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mChannelRotationDelaySpinner; + } + + /** * Updates the preferred tuner's combo box to reflect the current set of tuners. */ diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java index 074c2a4f..39da9e07 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java @@ -24,7 +24,6 @@ import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor; import io.github.dsheirer.gui.preference.duplicate.DuplicateCallPreferenceEditor; import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor; import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor; -import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor; import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor; import io.github.dsheirer.preference.UserPreferences; import javafx.scene.Node; @@ -50,8 +49,6 @@ public class PreferenceEditorFactory return new DirectoryPreferenceEditor(userPreferences); case JMBE_LIBRARY: return new JmbeLibraryPreferenceEditor(userPreferences); - case SOURCE_CHANNEL_MULTIPLE_FREQUENCY: - return new ChannelMultipleFrequencyPreferenceEditor(userPreferences); case SOURCE_TUNER_CHANNELIZER: return new TunerPreferenceEditor(userPreferences); case TALKGROUP_FORMAT: diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java index 4b5eda57..de2263fd 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java @@ -30,7 +30,6 @@ public enum PreferenceEditorType AUDIO_RECORD("Record"), AUDIO_OUTPUT("Output/Tones"), AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"), - SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"), SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"), TALKGROUP_FORMAT("Talkgroup & Radio ID"), DEFAULT("Default"); diff --git a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java index 07f72310..69e7e3ad 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java @@ -190,7 +190,6 @@ public class UserPreferencesEditor extends BorderPane storageItem.setExpanded(true); TreeItem sourceItem = new TreeItem<>("Source"); - sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_CHANNEL_MULTIPLE_FREQUENCY)); sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_TUNER_CHANNELIZER)); treeRoot.getChildren().add(sourceItem); sourceItem.setExpanded(true); @@ -247,7 +246,7 @@ public class UserPreferencesEditor extends BorderPane Menu viewMenu = new Menu("View"); MenuItem playlistEditorItem = new MenuItem("Playlist Editor"); - playlistEditorItem.setOnAction(event -> MyEventBus.getEventBus().post(new ViewPlaylistRequest())); + playlistEditorItem.setOnAction(event -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest())); viewMenu.getItems().add(playlistEditorItem); mMenuBar.getMenus().add(viewMenu); } diff --git a/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java index ef4606fd..36f45629 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java @@ -82,13 +82,18 @@ public class JmbeLibraryPreferenceEditor extends VBox mUserPreferences = userPreferences; //Register to receive directory preference update notifications so we can update the path labels - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); setPadding(new Insets(10,10,10,10)); setSpacing(10); getChildren().addAll(getEditorPane(), getButtonsBox(), getAlertUserWhenMissingCheckBox()); } + public void dispose() + { + MyEventBus.getGlobalEventBus().unregister(this); + } + private CheckBox getAlertUserWhenMissingCheckBox() { if(mAlertUserWhenMissingCheckBox == null) @@ -214,7 +219,7 @@ public class JmbeLibraryPreferenceEditor extends VBox if(buttonType == ButtonType.YES) { mLog.info("Posting JMBE editor request to event bus"); - MyEventBus.getEventBus().post(new JmbeEditorRequest(release)); + MyEventBus.getGlobalEventBus().post(new JmbeEditorRequest(release)); } }); } diff --git a/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java index 60973547..2a5c7e7e 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java @@ -98,12 +98,17 @@ public class DirectoryPreferenceEditor extends HBox mDirectoryPreference = userPreferences.getDirectoryPreference(); //Register to receive directory preference update notifications so we can update the path labels - MyEventBus.getEventBus().register(this); + MyEventBus.getGlobalEventBus().register(this); HBox.setHgrow(getEditorPane(), Priority.ALWAYS); getChildren().add(getEditorPane()); } + public void dispose() + { + MyEventBus.getGlobalEventBus().unregister(this); + } + private GridPane getEditorPane() { if(mEditorPane == null) diff --git a/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java deleted file mode 100644 index 8350f209..00000000 --- a/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * 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 - * ***************************************************************************** - */ - -package io.github.dsheirer.gui.preference.tuner; - -import io.github.dsheirer.preference.UserPreferences; -import io.github.dsheirer.preference.source.ChannelMultiFrequencyPreference; -import javafx.geometry.Insets; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Spinner; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; - - -/** - * Preference settings for channel source configurations containing multiple frequencies - */ -public class ChannelMultipleFrequencyPreferenceEditor extends HBox -{ - private static final String HELP_TEXT = "Rotation delay determines how long a decoder will wait before rotating " + - "to the next frequency in a list for a rotating control channel configuration."; - - private static final Insets MARGINS = new Insets(5,5,5,5); - private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference; - private GridPane mEditorPane; - private Label mRotationDelayLabel; - private Spinner mRotationDelaySpinner; - private Button mResetButton; - private Label mHelpText; - - public ChannelMultipleFrequencyPreferenceEditor(UserPreferences userPreferences) - { - mChannelMultiFrequencyPreference = userPreferences.getChannelMultiFrequencyPreference(); - getChildren().add(getEditorPane()); - } - - private GridPane getEditorPane() - { - if(mEditorPane == null) - { - mEditorPane = new GridPane(); - GridPane.setMargin(getRotationDelaySpinner(), new Insets(5, 5, 5, 5)); - mEditorPane.add(getRotationDelaySpinner(), 0, 0); - GridPane.setMargin(getRotationDelayLabel(), new Insets(5, 5, 5, 5)); - mEditorPane.add(getRotationDelayLabel(), 1, 0); - GridPane.setMargin(getResetButton(), MARGINS); - mEditorPane.add(getResetButton(), 2, 0); - GridPane.setMargin(getHelpText(), MARGINS); - mEditorPane.add(getHelpText(), 0, 1, 3, 1); - } - - return mEditorPane; - } - - public Spinner getRotationDelaySpinner() - { - if(mRotationDelaySpinner == null) - { - int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000); - mRotationDelaySpinner = new Spinner<>(1, 60, currentDelay, 1); - mRotationDelaySpinner.setEditable(true); - mRotationDelaySpinner.valueProperty().addListener((observable, oldValue, newValue) -> { - mChannelMultiFrequencyPreference.setRotationDelay(newValue * 1000); - }); - } - - return mRotationDelaySpinner; - } - - public Label getRotationDelayLabel() - { - if(mRotationDelayLabel == null) - { - mRotationDelayLabel = new Label("Rotation Delay (seconds)"); - } - - return mRotationDelayLabel; - } - - public Button getResetButton() - { - if(mResetButton == null) - { - mResetButton = new Button("Reset"); - mResetButton.setOnAction(event -> { - mChannelMultiFrequencyPreference.resetRotationDelay(); - int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000); - getRotationDelaySpinner().getValueFactory().setValue(currentDelay); - }); - } - - return mResetButton; - } - - public Label getHelpText() - { - if(mHelpText == null) - { - mHelpText = new Label(HELP_TEXT); - mHelpText.setWrapText(true); - } - - return mHelpText; - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java index a2962a4b..17637796 100644 --- a/src/main/java/io/github/dsheirer/identifier/Form.java +++ b/src/main/java/io/github/dsheirer/identifier/Form.java @@ -27,6 +27,9 @@ package io.github.dsheirer.identifier; public enum Form { ALIAS_LIST, + ARS_DEVICE, + ARS_USER, + ARS_PASSWORD, CALL_PROGRESS_TONE, CHANNEL, CHANNEL_DESCRIPTOR, @@ -39,9 +42,11 @@ public enum Form FULLY_QUALIFIED_IDENTIFIER, IPV4_ADDRESS, KNOX_TONE, + LOCATION, LOCATION_REGISTRATION_AREA, LOJACK, NEIGHBOR_SITE, + NETWORK, NETWORK_ACCESS_CODE, PATCH_GROUP, RADIO, @@ -54,6 +59,7 @@ public enum Form TALKGROUP, TELEPHONE_NUMBER, TONE, + UDP_PORT, UNIT_IDENTIFIER, UNIT_STATUS, USER_STATUS, diff --git a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java index 9d7a168e..3e981cbb 100644 --- a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * (Immutable) Collection of identifiers with convenient accessor methods @@ -36,7 +37,7 @@ import java.util.List; public class IdentifierCollection { private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class); - protected List mIdentifiers = new ArrayList<>(); + protected List mIdentifiers = new CopyOnWriteArrayList<>(); protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier; private int mTimeslot = 0; diff --git a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java index 211cd4da..b2814c7b 100644 --- a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java @@ -27,8 +27,9 @@ import io.github.dsheirer.sample.Listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; +import java.util.List; /** * Identifier collection with methods for changing or updating managed identifiers @@ -260,12 +261,11 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void clear() { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(); - while(it.hasNext()) + for(Identifier identifier: identifiers) { - notifyRemove(it.next()); - it.remove(); + remove(identifier); } } @@ -274,18 +274,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void remove(IdentifierClass identifierClass) { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(mIdentifiers); - Identifier next = null; - - while(it.hasNext()) + for(Identifier identifier: identifiers) { - next = it.next(); - - if(next.getIdentifierClass() == identifierClass) + if(identifier.getIdentifierClass() == identifierClass) { - it.remove(); - notifyRemove(next); + remove(identifier); } } } @@ -295,18 +290,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void remove(Form form) { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(mIdentifiers); - Identifier next = null; - - while(it.hasNext()) + for(Identifier identifier: identifiers) { - next = it.next(); - - if(next.getForm() == form) + if(identifier.getForm() == form) { - it.remove(); - notifyRemove(next); + remove(identifier); } } } @@ -316,18 +306,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void remove(Role role) { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(mIdentifiers); - Identifier next = null; - - while(it.hasNext()) + for(Identifier identifier: identifiers) { - next = it.next(); - - if(next.getRole() == role) + if(identifier.getRole() == role) { - it.remove(); - notifyRemove(next); + remove(identifier); } } } @@ -337,18 +322,15 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void remove(IdentifierClass identifierClass, Form form, Role role) { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(mIdentifiers); - Identifier next = null; - - while(it.hasNext()) + for(Identifier identifier: identifiers) { - next = it.next(); - - if(next.getIdentifierClass() == identifierClass && next.getForm() == form && next.getRole() == role) + if(identifier.getIdentifierClass() == identifierClass && + identifier.getForm() == form && + identifier.getRole() == role) { - it.remove(); - notifyRemove(next); + remove(identifier); } } } @@ -358,18 +340,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements */ public void remove(IdentifierClass identifierClass, Role role) { - Iterator it = mIdentifiers.iterator(); + List identifiers = new ArrayList<>(mIdentifiers); - Identifier next = null; - - while(it.hasNext()) + for(Identifier identifier: identifiers) { - next = it.next(); - - if(next.getIdentifierClass() == identifierClass && next.getRole() == role) + if(identifier.getIdentifierClass() == identifierClass && identifier.getRole() == role) { - it.remove(); - notifyRemove(next); + remove(identifier); } } } diff --git a/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java b/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java index 981d7071..7a0a9871 100644 --- a/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java @@ -26,18 +26,25 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.protocol.Protocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * Channel state identifier. Reflects the current state of the channel. + */ public class ChannelStateIdentifier extends Identifier { - public static final ChannelStateIdentifier ACTIVE = ChannelStateIdentifier.create(State.ACTIVE); - public static final ChannelStateIdentifier CALL = ChannelStateIdentifier.create(State.CALL); - public static final ChannelStateIdentifier CONTROL = ChannelStateIdentifier.create(State.CONTROL); - public static final ChannelStateIdentifier DATA = ChannelStateIdentifier.create(State.DATA); - public static final ChannelStateIdentifier ENCRYPTED = ChannelStateIdentifier.create(State.ENCRYPTED); - public static final ChannelStateIdentifier FADE = ChannelStateIdentifier.create(State.FADE); - public static final ChannelStateIdentifier IDLE = ChannelStateIdentifier.create(State.IDLE); - public static final ChannelStateIdentifier TEARDOWN = ChannelStateIdentifier.create(State.TEARDOWN); - public static final ChannelStateIdentifier RESET = ChannelStateIdentifier.create(State.RESET); + private final static Logger mLog = LoggerFactory.getLogger(ChannelStateIdentifier.class); + + public static final ChannelStateIdentifier ACTIVE = new ChannelStateIdentifier(State.ACTIVE); + public static final ChannelStateIdentifier CALL = new ChannelStateIdentifier(State.CALL); + public static final ChannelStateIdentifier CONTROL = new ChannelStateIdentifier(State.CONTROL); + public static final ChannelStateIdentifier DATA = new ChannelStateIdentifier(State.DATA); + public static final ChannelStateIdentifier ENCRYPTED = new ChannelStateIdentifier(State.ENCRYPTED); + public static final ChannelStateIdentifier FADE = new ChannelStateIdentifier(State.FADE); + public static final ChannelStateIdentifier IDLE = new ChannelStateIdentifier(State.IDLE); + public static final ChannelStateIdentifier TEARDOWN = new ChannelStateIdentifier(State.TEARDOWN); + public static final ChannelStateIdentifier RESET = new ChannelStateIdentifier(State.RESET); public ChannelStateIdentifier(State value) { @@ -53,9 +60,32 @@ public class ChannelStateIdentifier extends Identifier /** * Creates a decoder state identifier from the decoder state value */ - public static ChannelStateIdentifier create(State state) + public static ChannelStateIdentifier get(State state) { - return new ChannelStateIdentifier(state); + switch(state) + { + case ACTIVE: + return ACTIVE; + case CALL: + return CALL; + case CONTROL: + return CONTROL; + case DATA: + return DATA; + case ENCRYPTED: + return ENCRYPTED; + case FADE: + return FADE; + case IDLE: + return IDLE; + case RESET: + return RESET; + case TEARDOWN: + return TEARDOWN; + default: + mLog.warn("Creating new channel state identifier for unrecognized state [" + state + "]"); + return new ChannelStateIdentifier(state); + } } @Override diff --git a/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java b/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java new file mode 100644 index 00000000..4e4619a2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java @@ -0,0 +1,41 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.identifier.location; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; + +/** + * Location (point) identifier + */ +public abstract class LocationIdentifier extends Identifier +{ + /** + * Constructs an instance + * @param value location + * @param role to or from + */ + public LocationIdentifier(Point value, Role role) + { + super(value, IdentifierClass.USER, Form.LOCATION, role); + } +} diff --git a/src/main/java/io/github/dsheirer/identifier/location/Point.java b/src/main/java/io/github/dsheirer/identifier/location/Point.java new file mode 100644 index 00000000..57aa1884 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/location/Point.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.identifier.location; + +/** + * Geospatial point (location) + */ +public class Point +{ + private double mLatitude; + private double mLongitude; + + /** + * Constructs an instance + * @param latitude in decimal degrees + * @param longitude in decimal degrees + */ + public Point(double latitude, double longitude) + { + mLatitude = latitude; + mLongitude = longitude; + } + + public double getLatitude() + { + return mLatitude; + } + + public double getLongitude() + { + return mLongitude; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(Math.abs(mLatitude)).append(mLatitude >= 0 ? "N " : "S "); + sb.append(Math.abs(mLongitude)).append(mLongitude >= 0 ? "E" : "W"); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java b/src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java similarity index 77% rename from src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java rename to src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java index 983cc6fb..ed4961c7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java @@ -18,29 +18,20 @@ * ***************************************************************************** */ -package io.github.dsheirer.module.decode.ltrnet.identifier; +package io.github.dsheirer.identifier.site; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.integer.IntegerIdentifier; -import io.github.dsheirer.protocol.Protocol; -public class SiteIdentifier extends IntegerIdentifier +/** + * Radio site identifier + */ +public abstract class SiteIdentifier extends IntegerIdentifier { public SiteIdentifier(int site) { super(site, IdentifierClass.NETWORK, Form.SITE, Role.BROADCAST); } - - @Override - public Protocol getProtocol() - { - return Protocol.LTR_NET; - } - - public static SiteIdentifier create(int site) - { - return new SiteIdentifier(site); - } } diff --git a/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java b/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java index 419dc77f..b215d971 100644 --- a/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java +++ b/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java @@ -164,7 +164,7 @@ public class JmbeEditor extends VBox alert.initOwner(getCreateButton().getScene().getWindow()); alert.showAndWait().ifPresent(buttonType -> { jmbeCreator.completeProperty().unbind(); - MyEventBus.getEventBus().post(new JmbeEditorRequest(true)); + MyEventBus.getGlobalEventBus().post(new JmbeEditorRequest(true)); }); }); jmbeCreator.execute(); diff --git a/src/main/java/io/github/dsheirer/log/ApplicationLog.java b/src/main/java/io/github/dsheirer/log/ApplicationLog.java index 4d15bcbf..d04cf97a 100644 --- a/src/main/java/io/github/dsheirer/log/ApplicationLog.java +++ b/src/main/java/io/github/dsheirer/log/ApplicationLog.java @@ -62,7 +62,6 @@ public class ApplicationLog public ApplicationLog(UserPreferences userPreferences) { mUserPreferences = userPreferences; - MyEventBus.getEventBus().register(this); } @Subscribe @@ -87,6 +86,8 @@ public class ApplicationLog */ public void start() { + MyEventBus.getGlobalEventBus().register(this); + if(mRollingFileAppender == null) { mApplicationLogPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog(); @@ -169,6 +170,7 @@ public class ApplicationLog */ public void stop() { + MyEventBus.getGlobalEventBus().unregister(this); if(mRollingFileAppender != null) { mLog.info("Stopping application logging"); diff --git a/src/main/java/io/github/dsheirer/message/MessageHistory.java b/src/main/java/io/github/dsheirer/message/MessageHistory.java new file mode 100644 index 00000000..49cb39ff --- /dev/null +++ b/src/main/java/io/github/dsheirer/message/MessageHistory.java @@ -0,0 +1,72 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.message; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.module.HistoryModule; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Decode event history module. Maintains a history of decode events and constrains the total history size. + */ +public class MessageHistory extends HistoryModule implements IMessageListener +{ + private final static Logger mLog = LoggerFactory.getLogger(MessageHistory.class); + + /** + * Constructs an instance + */ + public MessageHistory(int historySize) + { + super(historySize); + } + + /** + * Implements the IDecodeEventListener interface - delegates to receive(event) method. + */ + @Override + public Listener getMessageListener() + { + return this; + } + + /** + * Process a request for message history and post the response to the module event bus + */ + @Subscribe + public void process(MessageHistoryRequest request) + { + getInterModuleEventBus().post(new MessageHistoryResponse(getItems())); + } + + /** + * Processes a request to preload message history + */ + @Subscribe + public void process(MessageHistoryPreloadData preloadData) + { + for(IMessage message: preloadData.getData()) + { + receive(message); + } + } +} diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java b/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java new file mode 100644 index 00000000..af1c3903 --- /dev/null +++ b/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java @@ -0,0 +1,39 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.message; + +import io.github.dsheirer.controller.channel.event.PreloadDataContent; + +import java.util.List; + +/** + * Message history to preload into the message history module. + */ +public class MessageHistoryPreloadData extends PreloadDataContent> +{ + /** + * Constructs an instance + * @param messages from the history + */ + public MessageHistoryPreloadData(List messages) + { + super(messages); + } +} diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java b/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java new file mode 100644 index 00000000..aebfdaa8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java @@ -0,0 +1,29 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.message; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request for message history from the message history module. + */ +public class MessageHistoryRequest extends ModuleEventBusMessage +{ +} diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java b/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java new file mode 100644 index 00000000..0b5fa3a4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java @@ -0,0 +1,49 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.message; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +import java.util.List; + +/** + * Response to message history request. + */ +public class MessageHistoryResponse extends ModuleEventBusMessage +{ + private List mMessages; + + /** + * Constructs an instance + * @param messages from the history + */ + public MessageHistoryResponse(List messages) + { + mMessages = messages; + } + + /** + * Message history + */ + public List getMessages() + { + return mMessages; + } +} diff --git a/src/main/java/io/github/dsheirer/message/MessageProviderModule.java b/src/main/java/io/github/dsheirer/message/MessageProviderModule.java index 081139af..94172e98 100644 --- a/src/main/java/io/github/dsheirer/message/MessageProviderModule.java +++ b/src/main/java/io/github/dsheirer/message/MessageProviderModule.java @@ -55,12 +55,6 @@ public class MessageProviderModule extends Module implements IMessageProvider } - @Override - public void dispose() - { - - } - public void receive(IMessage message) { if(mMessageListener != null) diff --git a/src/main/java/io/github/dsheirer/message/SyncLossMessage.java b/src/main/java/io/github/dsheirer/message/SyncLossMessage.java index 9740b980..7584c26c 100644 --- a/src/main/java/io/github/dsheirer/message/SyncLossMessage.java +++ b/src/main/java/io/github/dsheirer/message/SyncLossMessage.java @@ -30,6 +30,22 @@ public class SyncLossMessage extends Message { private int mBitsProcessed; private Protocol mProtocol; + private int mTimeslot = 0; + + /** + * Constructs a sync loss message. + * + * @param timestamp of the message + * @param bitsProcessed without a sync pattern detection of message decode + * @param timeslot that lost the sync + */ + public SyncLossMessage(long timestamp, int bitsProcessed, Protocol protocol, int timeslot) + { + super(timestamp); + mBitsProcessed = bitsProcessed; + mProtocol = protocol; + mTimeslot = timeslot; + } /** * Constructs a sync loss message. @@ -39,13 +55,21 @@ public class SyncLossMessage extends Message */ public SyncLossMessage(long timestamp, int bitsProcessed, Protocol protocol) { - super(timestamp); - mBitsProcessed = bitsProcessed; - mProtocol = protocol; + this(timestamp, bitsProcessed, protocol, 0); } /** - * Indicates the number of bits that were processed without a sync dectection or message decode + * Timeslot that lost the sync + * @return timeslot or a default of 0 + */ + @Override + public int getTimeslot() + { + return mTimeslot; + } + + /** + * Indicates the number of bits that were processed without a sync detection or message decode */ public int getBitsProcessed() { diff --git a/src/main/java/io/github/dsheirer/module/HistoryModule.java b/src/main/java/io/github/dsheirer/module/HistoryModule.java new file mode 100644 index 00000000..889b229c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/HistoryModule.java @@ -0,0 +1,101 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module; + +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base history module. Maintains a history of items and constrains the total history size. Adds support + * for registering a listener to receive a copy of new items as they arrive. + * + * Note: internal history items are de-duplicated. However, all items are passed through to the listener. + */ +public abstract class HistoryModule extends Module implements Listener +{ + private List mItems = new ArrayList<>(); + private Broadcaster mBroadcaster = new Broadcaster<>(); + private int mMaximumHistorySize; + + /** + * Constructs an instance + */ + public HistoryModule(int maximumHistorySize) + { + mMaximumHistorySize = maximumHistorySize; + } + + /** + * Access a copy of the events from this event history + */ + public List getItems() + { + return new ArrayList<>(mItems); + } + + @Override + public void reset() + { + mItems.clear(); + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + mItems.clear(); + mBroadcaster.clear(); + } + + /** + * Adds the listener to receive a copy of all items received by this history. + * @param listener to receive items, or pass null to clear existing listener. + */ + public void addListener(Listener listener) + { + mBroadcaster.addListener(listener); + } + + /** + * Primary item receiver method. + */ + @Override + public void receive(T item) + { + while(mItems.size() > mMaximumHistorySize) + { + mItems.remove(0); + } + + if(!mItems.contains(item)) + { + mItems.add(item); + } + + mBroadcaster.broadcast(item); + } +} diff --git a/src/main/java/io/github/dsheirer/module/Module.java b/src/main/java/io/github/dsheirer/module/Module.java index 9137643b..e88a6434 100644 --- a/src/main/java/io/github/dsheirer/module/Module.java +++ b/src/main/java/io/github/dsheirer/module/Module.java @@ -18,32 +18,86 @@ package io.github.dsheirer.module; +import com.google.common.eventbus.EventBus; + +/** + * Defines the basic component level class for all processing, demodulation and decoding components that can operate + * within a processing chain. + * + * Modules can optionally implement any of the following legacy interfaces: + * + * IAudioPacketListener consumes audio packets + * IAudioPacketProvider produces audio packets + * ICallEventListener consumes call events + * ICallEventProvider provides call events + * IChannelEventListener consumes channel events + * IChannelEventProvider provides channel events (reset, selection, start, etc) + * IFrequencyCorrectionController provides tuned frequency error corrections + * IMessageProvider produces messages + * IRealBufferListener consumes demodulated real sample buffers + * IRealBufferListener produces demodulated real sample buffers + * IReusableComplexBufferListener consumes complex samples and normally produces demodulated real sample buffers + * ISquelchStateListener consumes squelch states + * ISquelchStateProvider provides squelch states + * + * TODO: convert all pub/sub interfaces to use the inter-module event bus for event broadcast/subscribe and remove + * all of these interfaces. + */ public abstract class Module { + /** - * Defines the basic component level class for all processing, demodulation - * and decoding components that can operate within a processing chain. - * - * Modules can optionally implement any of the following interfaces: - * - * IAudioPacketListener consumes audio packets - * IAudioPacketProvider produces audio packets - * ICallEventListener consumes call events - * ICallEventProvider provides call events - * IChannelEventListener consumes channel events - * IChannelEventProvider provides channel events (reset, selection, start, etc) - * IFrequencyCorrectionController provides tuned frequency error corrections - * IMessageProvider produces messages - * IRealBufferListener consumes demodulated real sample buffers - * IRealBufferListener produces demodulated real sample buffers - * IReusableComplexBufferListener consumes complex samples and normally produces demodulated real sample buffers - * ISquelchStateListener consumes squelch states - * ISquelchStateProvider provides squelch states + * Event bus for inter-module communication of processing chain events. Note: this is an externally provided + * resource, typically provided by the ProcessingChain parent for each module. + */ + private EventBus mInterModuleEventBus; + + /** + * Constructs an instance */ public Module() { } + /** + * Sets the event bus to be used for inter-module event broadcasting and subscribing. + * @param interModuleEventBus to use + */ + public void setInterModuleEventBus(EventBus interModuleEventBus) + { + //Unregister from the current event bus (if one exists) + if(hasInterModuleEventBus()) + { + getInterModuleEventBus().unregister(this); + } + + mInterModuleEventBus = interModuleEventBus; + + //Auto-register with the event bus + if(hasInterModuleEventBus()) + { + getInterModuleEventBus().register(this); + } + } + + /** + * Event bus for inter-module communication. Note: use hasEventBus() to check that the module is assigned a bus. + * @return event bus or null if one has not been established. + */ + protected EventBus getInterModuleEventBus() + { + return mInterModuleEventBus; + } + + /** + * Indicates if this module has an assigned event bus. + * @return true if event bus is non-null. + */ + protected boolean hasInterModuleEventBus() + { + return mInterModuleEventBus != null; + } + /** * Initialize or reset all internal states to default - prepare to start * processing or resume processing, potentially with a different source. @@ -68,6 +122,11 @@ public abstract class Module /** * Dispose of all resources and listeners and prepare for garbage collection */ - public abstract void dispose(); - + public void dispose() + { + if(hasInterModuleEventBus()) + { + getInterModuleEventBus().unregister(this); + } + } } diff --git a/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java b/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java new file mode 100644 index 00000000..cd2607b5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java @@ -0,0 +1,28 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module; + +/** + * Base module event bus message. All classes that are passed over a processing chain event bus should extend this + * base class. + */ +public abstract class ModuleEventBusMessage +{ +} diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index efc8bc88..25b825a1 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -18,6 +18,7 @@ */ package io.github.dsheirer.module; +import com.google.common.eventbus.EventBus; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.audio.AudioSegmentBroadcaster; @@ -35,6 +36,7 @@ import io.github.dsheirer.channel.state.IDecoderStateEventProvider; import io.github.dsheirer.channel.state.MultiChannelState; import io.github.dsheirer.channel.state.SingleChannelState; import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification; import io.github.dsheirer.controller.channel.ChannelEvent; import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventProvider; @@ -44,11 +46,12 @@ import io.github.dsheirer.identifier.IdentifierUpdateProvider; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageListener; import io.github.dsheirer.message.IMessageProvider; -import io.github.dsheirer.module.decode.event.DecodeEventModel; +import io.github.dsheirer.message.MessageHistory; +import io.github.dsheirer.module.decode.event.DecodeEventHistory; import io.github.dsheirer.module.decode.event.IDecodeEvent; import io.github.dsheirer.module.decode.event.IDecodeEventListener; import io.github.dsheirer.module.decode.event.IDecodeEventProvider; -import io.github.dsheirer.module.decode.event.MessageActivityModel; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; import io.github.dsheirer.module.log.EventLogger; import io.github.dsheirer.record.binary.BinaryRecorder; import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; @@ -76,6 +79,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -115,64 +119,105 @@ public class ProcessingChain implements Listener private Broadcaster mSquelchStateEventBroadcaster = new Broadcaster<>(); private AtomicBoolean mRunning = new AtomicBoolean(); private List mModules = new ArrayList<>(); - private DecodeEventModel mDecodeEventModel; + private DecodeEventHistory mDecodeEventHistory = new DecodeEventHistory(500); + private MessageHistory mMessageHistory = new MessageHistory(500); private AbstractChannelState mChannelState; - private MessageActivityModel mMessageActivityModel; + private EventBus mEventBus; protected Source mSource; /** * Creates a processing chain for managing a set of modules * - * @param channel + * @param channel with configuration details for this processing chain + * @param aliasModel for looking up aliases */ public ProcessingChain(Channel channel, AliasModel aliasModel) { + mEventBus = new EventBus("Processing Chain Event Bus - Channel: " + channel.getName()); + if(channel.getDecodeConfiguration().getTimeslotCount() == 1) { mChannelState = new SingleChannelState(channel, aliasModel); } else { - mChannelState = new MultiChannelState(channel, aliasModel, channel.getDecodeConfiguration().getTimeslotCount()); + mChannelState = new MultiChannelState(channel, aliasModel, channel.getDecodeConfiguration().getTimeslots()); } addModule(mChannelState); - mDecodeEventModel = new DecodeEventModel(); - addDecodeEventListener(mDecodeEventModel); + addModule(mDecodeEventHistory); + addModule(mMessageHistory); } + /** + * Event bus used for inter-module communication. + * @return event bus + */ + public EventBus getEventBus() + { + return mEventBus; + } + + /** + * Channel state + */ public AbstractChannelState getChannelState() { return mChannelState; } - public DecodeEventModel getDecodeEventModel() + /** + * Decode event history module. + */ + public DecodeEventHistory getDecodeEventHistory() { - return mDecodeEventModel; + return mDecodeEventHistory; } - public MessageActivityModel getMessageActivityModel() + /** + * Message history module + */ + public MessageHistory getMessageHistory() { - return mMessageActivityModel; + return mMessageHistory; } - - //TODO: should we introduce the concept of getTimeslot() to messages and events and then use that to vector the - //TODO: inbound stream to the appropriate message and event models? - - public void setMessageActivityModel(MessageActivityModel model) + /** + * Process a channel configuration change notification. + * + * Note: this will normally occur when a standard channel is converted to a traffic channel. + * @param notification of the change + */ + public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification) { - mMessageActivityModel = model; + getEventBus().post(notification); + } - addMessageListener(mMessageActivityModel); + /** + * Removes any module that is an instance of a TrafficChannelManager + */ + public void removeTrafficChannelManager() + { + Iterator it = mModules.iterator(); + + while(it.hasNext()) + { + if(it.next() instanceof TrafficChannelManager) + { + it.remove(); + } + } } public void dispose() { stop(); - for(Module module : mModules) + List modules = new ArrayList<>(mModules); + + for(Module module : modules) { + removeModule(module); module.dispose(); } @@ -280,7 +325,7 @@ public class ProcessingChain implements Listener public void addModule(Module module) { mModules.add(module); - + module.setInterModuleEventBus(getEventBus()); registerListeners(module); registerProviders(module); } @@ -293,7 +338,7 @@ public class ProcessingChain implements Listener { unregisterListeners(module); unregisterProviders(module); - + module.setInterModuleEventBus(null); mModules.remove(module); } @@ -777,25 +822,6 @@ public class ProcessingChain implements Listener return mDecoderStateEventBroadcaster; } - /** - * Adds the listener to receive decoded messages from all decoders. - */ - public void addMessageListener(Listener listener) - { - mMessageBroadcaster.addListener(listener); - } - - /** - * Adds the list of listeners to receive decoded messages from all decoders. - */ - public void addMessageListeners(List> listeners) - { - for(Listener listener : listeners) - { - mMessageBroadcaster.addListener(listener); - } - } - /** * Adds a listener to receive source events from this processing chain */ @@ -804,14 +830,6 @@ public class ProcessingChain implements Listener mSourceEventBroadcaster.addListener(listener); } - /** - * Removes the listener from receiving decoded messages from all decoders. - */ - public void removeMessageListener(Listener listener) - { - mMessageBroadcaster.removeListener(listener); - } - /** * Adds the listener to receive frequency change events from the processing chain * diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java index 9cedf003..f90acfe1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java @@ -19,7 +19,6 @@ package io.github.dsheirer.module.decode; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageProvider; -import io.github.dsheirer.message.Message; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; @@ -36,12 +35,6 @@ public abstract class Decoder extends Module implements IMessageProvider { } - @Override - public void dispose() - { - mMessageListener = null; - } - public void start() { //no-op diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 2fed3147..9a71bdf0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode; import io.github.dsheirer.alias.AliasList; 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.state.AlwaysUnsquelchedDecoderState; import io.github.dsheirer.channel.state.State; @@ -38,6 +39,11 @@ import io.github.dsheirer.module.decode.am.AMDecoder; import io.github.dsheirer.module.decode.am.DecodeConfigAM; import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration; import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.dmr.DMRDecoder; +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.message.voice.DMRAudioModule; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState; import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter; @@ -80,12 +86,13 @@ import io.github.dsheirer.module.decode.passport.PassportDecoderState; import io.github.dsheirer.module.decode.passport.PassportMessageFilter; import io.github.dsheirer.module.decode.tait.Tait1200Decoder; import io.github.dsheirer.module.decode.tait.Tait1200DecoderState; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule; import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; -import io.github.dsheirer.source.tuner.channel.ChannelRotationMonitor; +import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,18 +114,25 @@ public class DecoderFactory * @return list of configured decoders */ public static List getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel, - UserPreferences userPreferences) + UserPreferences userPreferences, TrafficChannelManager trafficChannelManager) { - List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences); + List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences, trafficChannelManager); modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration())); return modules; } /** * Constructs a primary decoder as specified in the decode configuration + * + * @param channelMapModel for channel map lookups + * @param channel configuration + * @param aliasModel for alias lookups + * @param userPreferences instance + * @param trafficChannelManager optional traffic channel manager to use + * @return list of modules to use for a processing chain */ - public static List getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, - AliasModel aliasModel, UserPreferences userPreferences) + public static List getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel, + UserPreferences userPreferences, TrafficChannelManager trafficChannelManager) { List modules = new ArrayList(); @@ -135,6 +149,10 @@ public class DecoderFactory case AM: processAM(channel, modules, aliasList, decodeConfig); break; + case DMR: + processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig, + trafficChannelManager); + break; case NBFM: processNBFM(channel, modules, aliasList, decodeConfig); break; @@ -145,7 +163,7 @@ public class DecoderFactory processLTRNet(channel, modules, aliasList, (DecodeConfigLTRNet) decodeConfig); break; case MPT1327: - processMPT1327(channelMapModel, channel, userPreferences, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig); + processMPT1327(channelMapModel, channel, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig); break; case PASSPORT: processPassport(channel, modules, aliasList, decodeConfig); @@ -207,7 +225,8 @@ public class DecoderFactory { List activeStates = new ArrayList<>(); activeStates.add(State.CONTROL); - modules.add(new ChannelRotationMonitor(activeStates, userPreferences)); + modules.add(new ChannelRotationMonitor(activeStates, + ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay())); } } @@ -221,7 +240,7 @@ public class DecoderFactory } } - private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, UserPreferences userPreferences, List modules, AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig) { + private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, List modules, AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig) { DecodeConfigMPT1327 mptConfig = decodeConfig; ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName()); Sync sync = mptConfig.getSync(); @@ -232,8 +251,8 @@ public class DecoderFactory // Set max segment audio sample length slightly above call timeout to // not create a new segment if the processing chain finishes a bit after // actual call timeout. - int maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000); - modules.add(new AudioModule(aliasList, maxAudioSegmentLengthMillis)); + long maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000); + modules.add(new AudioModule(aliasList, AbstractAudioModule.DEFAULT_TIMESLOT, maxAudioSegmentLengthMillis)); SourceType sourceType = channel.getSourceConfiguration().getSourceType(); if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES) @@ -258,7 +277,8 @@ public class DecoderFactory { List activeStates = new ArrayList<>(); activeStates.add(State.CONTROL); - modules.add(new ChannelRotationMonitor(activeStates, userPreferences)); + modules.add(new ChannelRotationMonitor(activeStates, + ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay())); } } @@ -319,6 +339,57 @@ public class DecoderFactory } } + /** + * Creates modules for DMR decoder setup. + * + * Note: on some DMR systems (e.g. Capacity+) we convert standard channels to traffic channels (when rest channel + * changes) and so we reuse the traffic channel manager from the converted channel. + * + * @param channel for the DMR decoder + * @param userPreferences for access to JMBE audio library + * @param modules list to add to + * @param aliasList for the audio module + * @param decodeConfig for the DMR configuration + * @param trafficChannelManager optional traffic channel manager to re-use + */ + private static void processDMR(Channel channel, UserPreferences userPreferences, List modules, + AliasList aliasList, DecodeConfigDMR decodeConfig, + TrafficChannelManager trafficChannelManager) + { + modules.add(new DMRDecoder(decodeConfig)); + + DMRTrafficChannelManager dmrTrafficChannelManager = null; + + 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)); + modules.add(new DMRAudioModule(userPreferences, aliasList, 1)); + modules.add(new DMRAudioModule(userPreferences, aliasList, 2)); + + //Add a channel rotation monitor when we have multiple control channel frequencies specified + if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency && + ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies()) + { + List activeStates = new ArrayList<>(); + activeStates.add(State.CONTROL); + modules.add(new ChannelRotationMonitor(activeStates, + ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay())); + } + } + /** * Constructs a list of auxiliary decoders, as specified in the configuration * @@ -419,6 +490,9 @@ public class DecoderFactory case PASSPORT: filters.add(new PassportMessageFilter()); break; + case DMR: + //filters.add(new DMR) //todo: not finished + break; default: break; } @@ -439,10 +513,12 @@ public class DecoderFactory { case AM: return new DecodeConfigAM(); - case LTR_NET: - return new DecodeConfigLTRNet(); + case DMR: + return new DecodeConfigDMR(); case LTR: return new DecodeConfigLTRStandard(); + case LTR_NET: + return new DecodeConfigLTRNet(); case MPT1327: return new DecodeConfigMPT1327(); case NBFM: @@ -472,6 +548,8 @@ public class DecoderFactory DecodeConfigAM origAM = (DecodeConfigAM)config; copyAM.setRecordAudio(origAM.getRecordAudio()); return copyAM; + case DMR: + return new DecodeConfigDMR(); case LTR_NET: DecodeConfigLTRNet originalLTRNet = (DecodeConfigLTRNet)config; DecodeConfigLTRNet copyLTRNet = new DecodeConfigLTRNet(); diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java index 1def3b1d..fb2dde3b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java @@ -32,6 +32,7 @@ public enum DecoderType { //Primary Decoders AM("AM", "AM", Protocol.UNKNOWN), + DMR("DMR","DMR", Protocol.DMR), LTR("LTR", "LTR", Protocol.LTR), LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET), MPT1327("MPT1327", "MPT1327", Protocol.MPT1327), @@ -62,8 +63,9 @@ public enum DecoderType */ public static EnumSet PRIMARY_DECODERS = EnumSet.of(DecoderType.AM, - DecoderType.LTR_NET, + DecoderType.DMR, DecoderType.LTR, + DecoderType.LTR_NET, DecoderType.MPT1327, DecoderType.NBFM, DecoderType.P25_PHASE1, @@ -82,17 +84,14 @@ public enum DecoderType /** * Decoders that produce a (recordable) bitstream */ - public static final EnumSet BITSTREAM_DECODERS = - EnumSet.of(DecoderType.MPT1327, - DecoderType.P25_PHASE1, - DecoderType.P25_PHASE2); + public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR, + DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2); /** * Decoders that produce (recordable) MBE audio codec frames */ public static final EnumSet MBE_AUDIO_CODEC_DECODERS = - EnumSet.of(DecoderType.P25_PHASE1, - DecoderType.P25_PHASE2); + EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2); public Protocol getProtocol() { diff --git a/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java b/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java index da441c30..b6d9702d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java @@ -49,15 +49,6 @@ public abstract class PrimaryDecoder extends Decoder implements ISquelchStatePro return mDecodeConfiguration; } - @Override - public void dispose() - { - super.dispose(); - - mSquelchStateListener = null; - mDecodeConfiguration = null; - } - public void broadcast(SquelchStateEvent state) { if(mSquelchStateListener != null) diff --git a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java index 77432370..8249ae28 100644 --- a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java +++ b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import io.github.dsheirer.controller.config.Configuration; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.am.DecodeConfigAM; +import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet; import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard; import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327; @@ -48,7 +49,8 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification; @JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"), @JsonSubTypes.Type(value = DecodeConfigP25Phase1.class, name = "decodeConfigP25Phase1"), @JsonSubTypes.Type(value = DecodeConfigP25Phase2.class, name = "decodeConfigP25Phase2"), - @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport") + @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"), + @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR") }) @JacksonXmlRootElement(localName = "decode_configuration") public abstract class DecodeConfiguration extends Configuration @@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration { return 1; } + + @JsonIgnore + public int[] getTimeslots() + { + return new int[0]; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java new file mode 100644 index 00000000..7da5ff1e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java @@ -0,0 +1,520 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.DibitDelayBuffer; +import io.github.dsheirer.dsp.symbol.QPSKCarrierLock; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Processes a stream of Dibit symbols and performs burst detection and timeslot framing. This framer + * also detects abnormal PLL phase locks and issues PLL phase lock corrections. + * + * Timeslot Map: + * 0: Common Association Channel (CACH) and Slow Link Control (SLC) + * 1. Timeslot 1 + * 2. Timeslot 2 + */ +public class DMRBurstFramer implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRBurstFramer.class); + + private static final int DMR_SYMBOL_RATE = 4800; + private static final double PLL_PHASE_CORRECTION_90_DEGREES = (double)DMR_SYMBOL_RATE / 4.0; + private static final double PLL_PHASE_CORRECTION_180_DEGREES = (double)DMR_SYMBOL_RATE / 2.0; + + private static final int BURST_DIBIT_START_TS1 = 0; + private static final int BURST_DIBIT_START_TS2 = 144; + private static final int BURST_DIBIT_LENGTH = 144; + private static final int TWO_TIMESLOT_DIBIT_LENGTH = BURST_DIBIT_LENGTH * 2; + private static final int SYNC_DIBIT_OFFSET_TS1 = 66; + private static final int SYNC_DIBIT_OFFSET_TS2 = 210; + private static final int SYNC_DIBIT_LENGTH = 24; + private static final int MAX_STREAMING_SYNC_DETECT_BIT_ERRORS = 4; + private static final int MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS = 6; + + /** + * Threshold for issuing a sync loss message. This is set to trigger once the dibit count exceeds one second of + * dibits (4800 baud/dibits) plus two bursts dibit length. The two burst length padding is so that we don't reset + * the dibit counter below the burst sync check threshold. + */ + private static final int SYNC_LOSS_MESSAGE_THRESHOLD = DMR_SYMBOL_RATE + TWO_TIMESLOT_DIBIT_LENGTH; + + /** + * The message buffer is sized to hold two DMR bursts of 144 dibits (288 bits) each. + */ + private DibitDelayBuffer mMessageBuffer = new DibitDelayBuffer(BURST_DIBIT_LENGTH * 2); + + /** + * The sync delay buffer is sized to align sync detection with two bursts being fully loaded into the message and + * timeslot 1 region of the message buffer is aligned with the sync delay buffer. + * + * This delay equals the number of dibits in the second half of the burst payload: 54 dibits (108 bits). + */ + private DibitDelayBuffer mSyncDelayBuffer = new DibitDelayBuffer(54); + + /** + * Synchronized indicates that either the primary or secondary sync tracker is currently synchronized to + * one of the timeslot bursts. Once either sync tracker obtains a valid sync pattern, this flag is set + * to true. The synchronized state will remain true as long as at least one of the two sync trackers + * maintains sync state. Once both sync trackers lose sync, the framer will fallback into sync search/detection + * mode to inspect each dibit until a sync match can be (re)discovered. + */ + private boolean mSynchronized = false; + + /** + * Tracks the number of dibits received to trigger burst or sync loss message processing + */ + private int mDibitCounter = 0; + + /** + * The Sync detection is used to gain initial lock to a sync pattern or to regain sync if lost. The sync detector + * is used to align a DMR burst into the timeslot 1 region of the message dibit delay buffer so that it can be + * inspected to ensure the timeslots are aligned. + */ + private DMRSyncDetector mSyncDetectorTimeslot1 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS, + MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS); + private DMRSyncDetector mSyncDetectorTimeslot2 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS, + MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS); + + /** + * Timeslot sync trackers track the synchronization state for each of the two timeslots. + */ + private SyncTracker mSyncTrackerTimeslot1 = new SyncTracker(); + private SyncTracker mSyncTrackerTimeslot2 = new SyncTracker(); + + /** + * Timeslot alignment tracker tracks that the timeslots in the delay buffer are aligned. + */ + private TimeslotAlignmentTracker mTimeslotAlignmentTracker = new TimeslotAlignmentTracker(); + + private IDMRBurstDetectListener mBurstDetectListener; + private IPhaseLockedLoop mPhaseLockedLoop; + + /** + * Constructs an instance + * @param listener to be notified of framed burst detections and/or sync loss bits processed + * @param phaseLockedLoop to receive symbol alignment corrections + */ + public DMRBurstFramer(IDMRBurstDetectListener listener, IPhaseLockedLoop phaseLockedLoop) + { + mBurstDetectListener = listener; + mPhaseLockedLoop = phaseLockedLoop; + } + + /** + * Primary dibit symbol input method + * @param dibit to process + */ + @Override + public void receive(Dibit dibit) + { + mDibitCounter++; + + //Feed the message buffer first to ensure buffer contains two full bursts when a sync is detected + mMessageBuffer.put(dibit); + + //Feed the sync delay buffer and reassign the delayed dibit to feed the sync detector if we're not synchronized + dibit = mSyncDelayBuffer.getAndPut(dibit); + + if(mSynchronized) + { + if(mDibitCounter >= TWO_TIMESLOT_DIBIT_LENGTH) + { + if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH) + { + processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0); + } + + dispatch(); + } + } + else + { + mSyncDetectorTimeslot2.add(dibit); + + if(mSyncDetectorTimeslot2.hasSync()) + { + dispatch(); + } + else if(mDibitCounter >= SYNC_LOSS_MESSAGE_THRESHOLD) + { + processSyncLossDibits(DMR_SYMBOL_RATE, 0); + } + } + } + + /** + * Dispatches burst 1 and/or burst 2 that is currently in the message buffer + */ + private void dispatch() + { + if(mBurstDetectListener != null) + { + if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH) + { + processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0); + } + + mSyncDetectorTimeslot1.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS1)); + mSyncDetectorTimeslot2.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS2)); + + mSyncTrackerTimeslot1.update(mSyncDetectorTimeslot1.getSyncPattern(), mSyncDetectorTimeslot1.getCarrierLock()); + mSyncTrackerTimeslot2.update(mSyncDetectorTimeslot2.getSyncPattern(), mSyncDetectorTimeslot2.getCarrierLock()); + + CorrectedBinaryMessage burst1 = null; + CorrectedBinaryMessage burst2 = null; + + CACH cach1 = null; + CACH cach2 = null; + + //If we have valid sync but PLL tracker misalignment, correct both timeslots before we extract messages + if((mSyncTrackerTimeslot1.hasSync() && mSyncTrackerTimeslot1.hasCarrierMisAlignment()) || + (mSyncTrackerTimeslot2.hasSync() && mSyncTrackerTimeslot2.hasCarrierMisAlignment())) + { +// mLog.debug("Repairing Message Bits - PLL Mis-Alignment Detected"); + if(mSyncTrackerTimeslot1.hasCarrierMisAlignment()) + { + repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot1.getCarrierLock()); + repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot1.getCarrierLock()); + } + else + { + repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot2.getCarrierLock()); + repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot2.getCarrierLock()); + } + } + + if(mSyncTrackerTimeslot1.hasSync()) + { + burst1 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS1, BURST_DIBIT_LENGTH); + burst1.incrementCorrectedBitCount(mSyncDetectorTimeslot1.getPatternMatchBitErrorCount()); + + if(mSyncTrackerTimeslot1.getSyncPattern().hasCACH()) + { + cach1 = CACH.getCACH(burst1); + } + } + + if(mSyncTrackerTimeslot2.hasSync()) + { + burst2 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS2, BURST_DIBIT_LENGTH); + burst2.incrementCorrectedBitCount(mSyncDetectorTimeslot2.getPatternMatchBitErrorCount()); + + if(mSyncTrackerTimeslot2.getSyncPattern().hasCACH()) + { + cach2 = CACH.getCACH(burst2); + } + } + + //Update the timeslot alignment tracker using the sync patterns and optional CACHes from each timeslot + mTimeslotAlignmentTracker.update(cach1, cach2, mSyncDetectorTimeslot1.getSyncPattern(), + mSyncDetectorTimeslot2.getSyncPattern()); + + if(mTimeslotAlignmentTracker.hasSufficientData()) + { + if(mTimeslotAlignmentTracker.isAligned()) + { + if(burst1 != null) + { + mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 1); + mDibitCounter -= BURST_DIBIT_LENGTH; + } + else + { + processSyncLossDibits(BURST_DIBIT_LENGTH, 1); + } + + if(burst2 != null) + { + mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 2); + mDibitCounter -= BURST_DIBIT_LENGTH; + } + else + { + processSyncLossDibits(BURST_DIBIT_LENGTH, 2); + } + + mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync(); + } + else + { + if(burst1 != null) + { + mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 2); + mDibitCounter -= BURST_DIBIT_LENGTH; + + //Transfer the sync pattern from tracker 1 to tracker 2 so we don't lose voice framing + mSyncTrackerTimeslot2.setSyncPattern(mSyncTrackerTimeslot1.getSyncPattern()); + } + else + { + processSyncLossDibits(BURST_DIBIT_LENGTH, 2); + mSyncTrackerTimeslot2.reset(); + } + + mTimeslotAlignmentTracker.reset(); + mSyncTrackerTimeslot1.reset(); + mDibitCounter = BURST_DIBIT_LENGTH; + mSynchronized = true; + } + } + else + { + //When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they + //don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking + if(burst1 != null) + { + mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0); + mDibitCounter -= BURST_DIBIT_LENGTH; + } + else + { + processSyncLossDibits(BURST_DIBIT_LENGTH, 0); + } + + if(burst2 != null) + { + mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0); + mDibitCounter -= BURST_DIBIT_LENGTH; + } + else + { + processSyncLossDibits(BURST_DIBIT_LENGTH, 0); + } + + mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync(); + } + + if(!mSynchronized) + { + mTimeslotAlignmentTracker.reset(); + mSyncTrackerTimeslot1.reset(); + mSyncTrackerTimeslot2.reset(); + } + + if(mDibitCounter < 0) + { + mDibitCounter = 0; + } + } + } + + /** + * Repairs the captured dibits in the message buffer when a PLL lock misalignment is detected. + * @param offset to start of burst + * @param carrierLock that indicates the mis-alignment + */ + private void repairPLLMisalignment(int offset, QPSKCarrierLock carrierLock) + { + int end = offset + BURST_DIBIT_LENGTH; + + for(int x = offset; x < end; x++) + { + Dibit misalignedDibit = mMessageBuffer.get(x); + mMessageBuffer.set(x, carrierLock.correct(misalignedDibit)); + } + + if(mPhaseLockedLoop != null) + { +// mLog.warn("Repairing PLL Phase Lock Mis-Alignment - Detected: " + carrierLock); + switch(carrierLock) + { + case PLUS_90: + mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_90_DEGREES); + break; + case MINUS_90: + mPhaseLockedLoop.correctInversion(-PLL_PHASE_CORRECTION_90_DEGREES); + break; + case INVERTED: + mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_180_DEGREES); + break; + } + } + } + + /** + * Resets the message and sync delay buffers and resets the dibit counter to zero. + */ + public void reset() + { + mDibitCounter = 0; + mMessageBuffer.reset(); + mSyncDelayBuffer.reset(); + mTimeslotAlignmentTracker.reset(); + } + + /** + * Calculates the value of the sync field currently in the message buffer as specified by the offset value. + */ + private long getSyncValue(int dibitOffset) + { + Dibit[] syncDibits = mMessageBuffer.getBuffer(dibitOffset, SYNC_DIBIT_LENGTH); + + long value = 0; + + for(Dibit dibit: syncDibits) + { + value = Long.rotateLeft(value, 2); + value += dibit.getValue(); + } + + return value; + } + + /** + * Processes dibits received without a sync condition + * @param dibitCount with sync loss + */ + private void processSyncLossDibits(int dibitCount, int timeslot) + { + mDibitCounter -= dibitCount; + + if(mBurstDetectListener != null) + { + mBurstDetectListener.syncLost(dibitCount * 2, timeslot); + } + } + + /** + * Tracks the sync state for a timeslot to account for voice framing that does not transmit a sync pattern in each + * frame. + */ + public class SyncTracker + { + private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN; + private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL; + + /** + * Updates the sync pattern for the most recently detected burst to the specified pattern. When the previous + * sync pattern is one of either base or mobile voice frame and the detected pattern is unknown, automatically + * advances the sync pattern to the next voice frame in the super frame sequence to account for voice + * frames B - F that do not have a sync pattern. + */ + public void update(DMRSyncPattern pattern, QPSKCarrierLock carrierLock) + { + if(pattern == DMRSyncPattern.UNKNOWN) + { + switch(mSyncPattern) + { + case BASE_STATION_VOICE: //Voice frame A + mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_B; + break; + case BS_VOICE_FRAME_B: + mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_C; + break; + case BS_VOICE_FRAME_C: + mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_D; + break; + case BS_VOICE_FRAME_D: + mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_E; + break; + case BS_VOICE_FRAME_E: + mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_F; + break; + + case MOBILE_STATION_VOICE: //Voice frame A + case DIRECT_MODE_VOICE_TIMESLOT_1: + case DIRECT_MODE_VOICE_TIMESLOT_2: + mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_B; + break; + case MS_VOICE_FRAME_B: + mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_C; + break; + case MS_VOICE_FRAME_C: + mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_D; + break; + case MS_VOICE_FRAME_D: + mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_E; + break; + case MS_VOICE_FRAME_E: + mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_F; + break; + default: + mSyncPattern = pattern; + mCarrierLock = carrierLock; + break; + } + } + else + { + mSyncPattern = pattern; + mCarrierLock = carrierLock; + } + } + + /** + * Most recently detected sync pattern + */ + public DMRSyncPattern getSyncPattern() + { + return mSyncPattern; + } + + /** + * Explicitly sets the pattern for this tracker + */ + public void setSyncPattern(DMRSyncPattern pattern) + { + mSyncPattern = pattern; + } + + /** + * Carrier lock as indicated by the sync detector. + */ + public QPSKCarrierLock getCarrierLock() + { + return mCarrierLock; + } + + /** + * Indicates if the burst has a QPSK constellation mis-alignment that has to be corrected + */ + public boolean hasCarrierMisAlignment() + { + return getCarrierLock() != QPSKCarrierLock.NORMAL; + } + + /** + * Checks the contents of the message buffer to determine if the message has a valid sync pattern, or if the + * message is a continuation voice frame from a voice super frame. The identified sync pattern and number of + * bit errors for that pattern are stored in the sync detector so that they can be accessed for dispatching + * a message. + */ + public boolean hasSync() + { + return getSyncPattern() != DMRSyncPattern.UNKNOWN; + } + + /** + * Resets the sync pattern to unknown + */ + public void reset() + { + mSyncPattern = DMRSyncPattern.UNKNOWN; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java new file mode 100644 index 00000000..0f1de917 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java @@ -0,0 +1,344 @@ +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; +import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor; +import io.github.dsheirer.dsp.psk.pll.PLLBandwidth; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IReusableByteBufferProvider; +import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; +import io.github.dsheirer.sample.buffer.ReusableByteBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProvider; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * DMR decoder module. + */ +public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider, + IReusableComplexBufferListener, Listener, IReusableByteBufferProvider +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRDecoder.class); + protected static final float SAMPLE_COUNTER_GAIN = 0.4f; + private static final double SYMBOL_RATE = 4800.0; + private double mSampleRate; + private Broadcaster mDibitBroadcaster = new Broadcaster<>(); + private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300); + private DMRMessageProcessor mMessageProcessor; + private Listener mSourceEventListener; + private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); + private Map mBasebandFilters = new HashMap<>(); + private ComplexFIRFilter2 mBasebandFilter; + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator; + protected CostasLoop mCostasLoop; + protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor; + protected DMRMessageFramer mMessageFramer; + + /** + * Constructs an instance + */ + public DMRDecoder(DecodeConfigDMR config) + { + mMessageProcessor = new DMRMessageProcessor(config); + mMessageProcessor.setMessageListener(getMessageListener()); + getDibitBroadcaster().addListener(mByteBufferAssembler); + setSampleRate(25000.0); + } + + /** + * DMR decoder type. + */ + @Override + public DecoderType getDecoderType() + { + return DecoderType.DMR; + } + + /** + * Message processor + */ + protected DMRMessageProcessor getMessageProcessor() + { + return mMessageProcessor; + } + + /** + * Sets the sample rate and configures internal decoder components. + * @param sampleRate + */ + public void setSampleRate(double sampleRate) + { + if(sampleRate <= getSymbolRate() * 2) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " + + getSymbolRate() + " symbol rate)"); + } + + mSampleRate = sampleRate; + mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter()); + mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); + mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300); + mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); + + mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer); + + if(mMessageFramer != null) + { + getDibitBroadcaster().removeListener(mMessageFramer); + } + + //The Costas Loop receives symbol-inversion correction requests when detected. + //The PLL gain monitor receives sync detect/loss signals from the message framer + mMessageFramer = new DMRMessageFramer(mCostasLoop); + mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor); + mMessageFramer.setListener(getMessageProcessor()); + + mQPSKDemodulator.setSymbolListener(getDibitBroadcaster()); + getDibitBroadcaster().addListener(mMessageFramer); + } + + /** + * Primary method for processing incoming complex sample buffers + * @param reusableComplexBuffer containing channelized complex samples + */ + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + //User accounting of the incoming buffer is handled by the filter + ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer); + + //User accounting of the incoming buffer is handled by the gain filter + ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered); + + mMessageFramer.setCurrentTime(reusableComplexBuffer.getTimestamp()); + + //User accounting of the filtered buffer is handled by the demodulator + mQPSKDemodulator.receive(gainApplied); + } + + /** + * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * @param reusableComplexBuffer to filter + * @return filtered complex buffer + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + //User accounting of the incoming buffer is handled by the filter + return mBasebandFilter.filter(reusableComplexBuffer); + } + + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ + private float[] getBasebandFilter() + { + //Attempt to reuse a cached (ie already-designed) filter if available + float[] filter = mBasebandFilters.get(getSampleRate()); + + if(filter == null) + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate((int)getSampleRate()) + .passBandCutoff(5100) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandAmplitude(0.0) + .stopBandStart(6500) + .stopBandRipple(0.01) + .build(); + + try + { + filter = FilterFactory.getTaps(specification);// + } + catch(Exception fde) //FilterDesignException + { + mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); + } + + if(filter != null) + { + mBasebandFilters.put(getSampleRate(), filter); + } + else + { + throw new IllegalStateException("Couldn't design a DMR baseband filter for sample rate: " + getSampleRate()); + } + } + + return filter; + } + + /** + * Process source events + */ + protected void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //Reset the PLL if/when the tuner PPM changes so that we can re-lock + mCostasLoop.reset(); + break; + } + } + + /** + * Resets this decoder to prepare for processing a new channel + */ + @Override + public void reset() + { + mCostasLoop.reset(); + mFrequencyCorrectionSyncMonitor.reset(); + } + + /** + * Assembler for packaging Dibit stream into reusable byte buffers. + */ + protected Broadcaster getDibitBroadcaster() + { + return mDibitBroadcaster; + } + + /** + * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler + */ + @Override + public void setBufferListener(Listener listener) + { + mByteBufferAssembler.setBufferListener(listener); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler + */ + @Override + public void removeBufferListener(Listener listener) + { + mByteBufferAssembler.removeBufferListener(listener); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler + */ + @Override + public boolean hasBufferListeners() + { + return mByteBufferAssembler.hasBufferListeners(); + } + + protected double getSymbolRate() + { + return SYMBOL_RATE; + } + + /** + * Current sample rate for this decoder + */ + protected double getSampleRate() + { + return mSampleRate; + } + + /** + * Samples per symbol based on current sample rate and symbol rate. + */ + public float getSamplesPerSymbol() + { + return (float)(getSampleRate() / getSymbolRate()); + } + + /** + * Sets the source event listener to receive source events from this decoder. + */ + @Override + public void setSourceEventListener(Listener listener) + { + mSourceEventListener = listener; + } + + /** + * Removes a registered source event listener from receiving source events from this decoder + */ + @Override + public void removeSourceEventListener() + { + mSourceEventListener = null; + } + + @Override + public Listener getSourceEventListener() + { + return new Listener() + { + @Override + public void receive(SourceEvent sourceEvent) + { + process(sourceEvent); + } + }; + } + + /** + * Broadcasts the source event to an optional registered listener. This method should primarily be used to + * issue frequency correction requests to the channel source. + * @param sourceEvent to broadcast + */ + public void broadcast(SourceEvent sourceEvent) + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(sourceEvent); + } + } + + /** + * Listener interface to receive reusable complex buffers + */ + @Override + public Listener getReusableComplexBufferListener() + { + return DMRDecoder.this; + } + + /** + * Starts the decoder + */ + @Override + public void start() + { + //No-op + } + + /** + * Stops the decoder + */ + @Override + public void stop() + { + //No-op + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java new file mode 100644 index 00000000..40849285 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java @@ -0,0 +1,123 @@ +/* + * ****************************************************************************** + * sdrtrunk + * 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.complex.Complex; + +/** + * Instrumented version of DMR decoder + */ +public class DMRDecoderInstrumented extends DMRDecoder +{ + private Listener mPLLPhaseErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mComplexSymbolListener; + private Listener mFilteredSymbolListener; + private Listener mSymbolDecisionDataListener; + + /** + * Instrumented version of the DMR decoder that supports registering listeners to provide access to data as + * it is being processed by the decoder. + */ + public DMRDecoderInstrumented(DecodeConfigDMR config) + { + super(config); + } + + /** + * Overrides the filter method so that we can capture the filtered samples for instrumentation + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); + + if(mFilteredSymbolListener != null) + { + filtered.incrementUserCount(); + mFilteredSymbolListener.receive(filtered); + } + + return filtered; + } + + /** + * Overrides this method so we can correctly configure for instrumented operations + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + + InterpolatingSampleBufferInstrumented instrumentedBuffer = + new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN); + mInterpolatingSampleBuffer = instrumentedBuffer; + + DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); + mQPSKDemodulator = instrumented; + + instrumented.setComplexSymbolListener(mComplexSymbolListener); + instrumented.setPLLErrorListener(mPLLPhaseErrorListener); + instrumented.setPLLFrequencyListener(mPLLFrequencyListener); + instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); + instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + instrumented.setSymbolListener(getDibitBroadcaster()); + getDibitBroadcaster().addListener(mMessageFramer); + } + + public void setComplexSymbolListener(Listener listener) + { + mComplexSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + } + + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + } + + public void setFilteredBufferListener(Listener listener) + { + mFilteredSymbolListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + } + + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java new file mode 100644 index 00000000..d3876b7c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -0,0 +1,1135 @@ +/* + * + * * ****************************************************************************** + * * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao + * * + * * This 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 + * * ***************************************************************************** + * + * + */ +package io.github.dsheirer.module.decode.dmr; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent; +import io.github.dsheirer.channel.state.DecoderStateEvent; +import io.github.dsheirer.channel.state.DecoderStateEvent.Event; +import io.github.dsheirer.channel.state.State; +import io.github.dsheirer.channel.state.TimeslotDecoderState; +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.controller.channel.Channel.ChannelType; +import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification; +import io.github.dsheirer.identifier.Identifier; +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.integer.IntegerIdentifier; +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.event.DMRDecodeEvent; +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.motorola.CapacityMaxAloha; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSystemStatus; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Protect; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.Acknowledge; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.Ahoy; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.ServiceRadioCheck; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.StunReviveKill; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser; +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.CapacityPlusGroupVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel; +import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage; +import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; +import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; +import io.github.dsheirer.module.decode.event.DecodeEvent; +import io.github.dsheirer.module.decode.event.DecodeEventType; +import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest; +import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Decoder state for an DMR channel. Maintains the call/data/idle state of the channel and produces events by + * monitoring the decoded message stream. + */ +public class DMRDecoderState extends TimeslotDecoderState +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRDecoderState.class); + private static final long MAX_VALID_CALL_DURATION_MS = 30000; + private Channel mChannel; + private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor; + private DMRTrafficChannelManager mTrafficChannelManager; + private DecodeEvent mCurrentCallEvent; + private long mCurrentFrequency; + private Map mDetectedCallEventsMap = new TreeMap<>(); + private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST = + new AddChannelRotationActiveStateRequest(State.ACTIVE); + + /** + * Constructs an DMR decoder state with an optional traffic channel manager. + * @param channel with configuration details + * @param trafficChannelManager for handling traffic channel grants. + * @param configurationMonitor for tracking activity summary + */ + public DMRDecoderState(Channel channel, int timeslot, DMRTrafficChannelManager trafficChannelManager) + { + super(timeslot); + mChannel = channel; + mTrafficChannelManager = trafficChannelManager; + + //The decoder state passes all messages to the network configuration monitor, so we only contruct + //the monitor for timeslot 1. + if(timeslot == 1) + { + mNetworkConfigurationMonitor = new DMRNetworkConfigurationMonitor(channel); + } + } + + /** + * Indicates if this decoder state has an (optional) traffic channel manager. + */ + private boolean hasTrafficChannelManager() + { + return mTrafficChannelManager != null; + } + + /** + * 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, + * we nullify the traffic channel manager to ensure the traffic channel no longer behaves as a standard channel + * regarding channel conversions and allocations. + * + * @param notification of channel configuration change + */ + @Subscribe + public void channelChanged(ChannelConfigurationChangeNotification notification) + { + if(notification.getChannel().isTrafficChannel()) + { + mTrafficChannelManager = null; + } + } + + /** + * Identifies the decoder type + */ + @Override + public DecoderType getDecoderType() + { + return DecoderType.DMR; + } + + /** + * Performs a full reset to prepare this object for reuse on a new channel + */ + @Override + public void reset() + { + super.reset(); + resetState(); + + mCurrentFrequency = 0; + mDetectedCallEventsMap.clear(); + } + + /** + * Resets any temporal state details + */ + protected void resetState() + { + super.resetState(); + closeCurrentCallEvent(System.currentTimeMillis()); + } + + /** + * Primary message processing method. + */ + @Override + public void receive(IMessage message) + { + if(message.isValid() && message.getTimeslot() == getTimeslot()) + { + if(message instanceof VoiceMessage) + { + processVoice((VoiceMessage)message); + } + else if(message instanceof DataMessage) + { + processData((DataMessage)message); + } + else if(message instanceof LCMessage) + { + processLinkControl((LCMessage)message, false); + } + else if(message instanceof DMRPacketMessage) + { + processPacket((DMRPacketMessage)message); + } + else if(message instanceof DMRMessage) + { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + } + } + //SLCO messages on timeslot 0 to catch capacity plus rest channel events + else if(message.isValid() && message.getTimeslot() == 0 && message instanceof LCMessage) + { + processLinkControl((LCMessage)message, false); + } + + //Pass the message to the network configuration monitor, if this decoder state has a non-null instance + if(mNetworkConfigurationMonitor != null && message.isValid() && message instanceof DMRMessage) + { + mNetworkConfigurationMonitor.process((DMRMessage)message); + } + } + + /** + * Processes Capacity Plus rest channel notifications to detect when the rest channel has changed. When a new + * rest channel is specified that is different from the current channel, notify the traffic channel manager so that + * it can convert the current channel to a traffic channel and start a new channel for the rest channel, using the + * current channel's configuration details and the specified rest channel frequency. This approach ensures that + * there is no disruption to the currently processing channel and that the original channel configuration can be + * recreated to follow the rest channel. + * + * @param restChannel currently indicated + */ + private void updateRestChannel(DMRChannel restChannel) + { + //Only respond if this is a standard/control channel (not a traffic channel). + if(mChannel.isStandardChannel() && mCurrentFrequency > 0 && + restChannel.getDownlinkFrequency() > 0 && + restChannel.getDownlinkFrequency() != mCurrentFrequency && mTrafficChannelManager != null) + { + mTrafficChannelManager.convertToTrafficChannel(mChannel, mCurrentFrequency, restChannel, + mNetworkConfigurationMonitor); + } + } + + /** + * Preloads the DMR network configuration monitor that is optionally delivered as preload data. This is primarily + * used during a Capacity plus REST channel rotation to pass the monitor from the previous REST channel to the + * current REST channel to ensure data continuity. + * + * Note: the monitor is only assigned to the timeslot 1 decoder state since the decoder state passes all received + * messages (Timeslots 0, 1, and 2) to the monitor. + * + * Note: this method is invoked over the Guava event but by the ChannelProcessingManager. + * + * @param preloadData containing a DMR network configuration monitor. + */ + @Subscribe + public void preload(DMRNetworkConfigurationPreloadData preloadData) + { + if(getTimeslot() == 1 && preloadData.hasData()) + { + mNetworkConfigurationMonitor = preloadData.getData(); + } + } + + /** + * Processes a packet message + */ + private void processPacket(DMRPacketMessage packet) + { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot())); + + DecodeEvent packetEvent = DMRDecodeEvent.builder(packet.getTimestamp()) + .eventDescription(DecodeEventType.DATA_PACKET.name()) + .identifiers(getMergedIdentifierCollection(packet.getIdentifiers())) + .timeslot(packet.getTimeslot()) + .details(packet.toString()) + .build(); + + broadcast(packetEvent); + } + + /** + * Processes voice messages + */ + private void processVoice(VoiceMessage message) + { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); + } + + /** + * Processes a voice header message + */ + private void processHeader(HeaderMessage header) + { + switch(header.getSlotType().getDataType()) + { + case VOICE_HEADER: + broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot())); + break; + case PI_HEADER: + case MBC_HEADER: + case DATA_HEADER: + case USB_DATA: + case MBC_ENC_HEADER: + case DATA_ENC_HEADER: + case CHANNEL_CONTROL_ENC_HEADER: + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot())); + break; + } + + //Process the link control message to get the identifiers + LCMessage lc = header.getLCMessage(); + + if(lc.isValid()) + { + processLinkControl(lc, false); + } + } + + /** + * Process Data Messages + */ + private void processData(DataMessage message) + { + switch(message.getSlotType().getDataType()) + { + case CSBK: + if(message instanceof CSBKMessage) + { + processCSBK((CSBKMessage)message); + } + break; + case VOICE_HEADER: + if(message instanceof HeaderMessage) + { + processVoiceHeader((HeaderMessage)message); + } + break; + case USB_DATA: + break; + case PI_HEADER: + case MBC_HEADER: + case DATA_HEADER: + case MBC_ENC_HEADER: + case DATA_ENC_HEADER: + case CHANNEL_CONTROL_ENC_HEADER: + if(message instanceof HeaderMessage) + { + processHeader((HeaderMessage)message); + } + break; + case SLOT_IDLE: + closeCurrentCallEvent(message.getTimestamp()); + getIdentifierCollection().remove(IdentifierClass.USER); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + break; + case TLC: + if(message instanceof Terminator) + { + processTerminator((Terminator)message); + } + break; + case MBC_BLOCK: + case RATE_1_OF_2_DATA: + case RATE_3_OF_4_DATA: + case RATE_1_DATA: + case RESERVED_15: + case UNKNOWN: + default: + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + break; + } + } + + /** + * Process terminator with link control messages + */ + private void processTerminator(Terminator terminator) + { + closeCurrentCallEvent(terminator.getTimestamp()); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + + LCMessage lcMessage = terminator.getLCMessage(); + + if(lcMessage.isValid()) + { + processLinkControl(lcMessage, true); + } + } + + /** + * Process a voice header message + */ + private void processVoiceHeader(HeaderMessage voiceHeader) + { + LCMessage lcMessage = voiceHeader.getLCMessage(); + + if(lcMessage.isValid()) + { + processLinkControl(lcMessage, false); + } + } + + private void processCSBK(CSBKMessage csbk) + { + switch(csbk.getOpcode()) + { + case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_PAYLOAD: + case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_PAYLOAD: + if(csbk instanceof Acknowledge) + { + DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.RESPONSE.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details(((Acknowledge)csbk).getReason().toString()) + .build(); + + broadcast(ackEvent); + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + break; + case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_TSCC: + case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_TSCC: + if(csbk instanceof Acknowledge) + { + DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.RESPONSE.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details(((Acknowledge)csbk).getReason().toString()) + .build(); + + broadcast(ackEvent); + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_AHOY: + if(csbk instanceof Ahoy) + { + switch(((Ahoy)csbk).getServiceKind()) + { + case AUTHENTICATE_REGISTER_RADIO_CHECK_SERVICE: + DecodeEvent registerEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.COMMAND.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details(DecodeEventType.REGISTER.toString()) + .build(); + broadcast(registerEvent); + break; + case CANCEL_CALL_SERVICE: + DecodeEvent cancelEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.COMMAND.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("CANCEL CALL") + .build(); + broadcast(cancelEvent); + break; + case SUPPLEMENTARY_SERVICE: + if(csbk instanceof StunReviveKill) + { + DecodeEvent stunEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.COMMAND.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details(((StunReviveKill)csbk).getCommand() + " RADIO") + .build(); + broadcast(stunEvent); + } + break; + case FULL_DUPLEX_MS_TO_MS_PACKET_CALL_SERVICE: + case FULL_DUPLEX_MS_TO_MS_VOICE_CALL_SERVICE: + case INDIVIDUAL_VOICE_CALL_SERVICE: + case INDIVIDUAL_PACKET_CALL_SERVICE: + case INDIVIDUAL_UDT_SHORT_DATA_CALL_SERVICE: + case TALKGROUP_PACKET_CALL_SERVICE: + case TALKGROUP_UDT_SHORT_DATA_CALL_SERVICE: + case TALKGROUP_VOICE_CALL_SERVICE: + if(csbk instanceof ServiceRadioCheck) + { + ServiceRadioCheck src = (ServiceRadioCheck)csbk; + + DecodeEvent checkEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.RADIO_CHECK.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details(src.getServiceDescription() + " SERVICE FOR " + + (src.isTalkgroupTarget() ? "TALKGROUP" : "RADIO")) + .build(); + broadcast(checkEvent); + } + break; + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_ALOHA: + if(csbk instanceof Aloha) + { + Aloha aloha = (Aloha)csbk; + + if(aloha.hasRadioIdentifier()) + { + DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.RESPONSE.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("Aloha Acknowledge") + .build(); + + broadcast(ackEvent); + resetState(); + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_ANNOUNCEMENT: + if(csbk instanceof Announcement) + { + switch(((Announcement)csbk).getAnnouncementType()) + { + case MASS_REGISTRATION: + DecodeEvent massEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.REGISTER.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("MASS REGISTRATION") + .build(); + broadcast(massEvent); + break; + case VOTE_NOW_ADVICE: + if(csbk instanceof VoteNowAdvice) + { + DecodeEvent voteEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.COMMAND.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("VOTE NOW FOR " + ((VoteNowAdvice)csbk).getVotedSystemIdentityCode()) + .build(); + broadcast(voteEvent); + } + break; + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_CLEAR: + broadcast(new DecoderStateEvent(this, Event.END, State.CALL, getTimeslot())); + resetState(); + break; + case STANDARD_PREAMBLE: + getIdentifierCollection().update(csbk.getIdentifiers()); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot())); + break; + case STANDARD_PROTECT: + if(csbk instanceof Protect) + { + DecodeEvent protectEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.COMMAND.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("PROTECT: " + ((Protect)csbk).getProtectKind()) + .build(); + broadcast(protectEvent); + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); + break; + case HYTERA_08_ANNOUNCEMENT: + case HYTERA_68_ANNOUNCEMENT: + case HYTERA_XPT_SITE_STATE: + + case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT: + if(csbk instanceof CapacityPlusNeighbors) + { + //Update state and rest channel + updateRestChannel(((CapacityPlusNeighbors)csbk).getRestChannel()); + } + break; + case MOTOROLA_CAPPLUS_SYSTEM_STATUS: + if(csbk instanceof CapacityPlusSystemStatus) + { + CapacityPlusSystemStatus cpss = (CapacityPlusSystemStatus)csbk; + + //Channel rotation monitor normally uses only CONTROL state, so when we detect that we're a + //Capacity plus system, add ACTIVE as an active state to the monitor. This can be requested repeatedly. + getInterModuleEventBus().post(CAPACITY_PLUS_ACTIVE_STATE_REQUEST); + + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + + //Update state and rest channel + updateRestChannel(cpss.getRestChannel()); + } + break; + case MOTOROLA_CONPLUS_NEIGHBOR_REPORT: + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT: + case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM: + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM: + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM: + if(csbk instanceof ChannelGrant) + { + ChannelGrant dataGrant = (ChannelGrant)csbk; + DMRChannel channel = dataGrant.getChannel(); + if(hasTrafficChannelManager()) + { + mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + } + processCallDetection(dataGrant.getChannel(), dataGrant.getIdentifiers(), dataGrant.getTimestamp(), DecodeEventType.DATA_CALL); + } + break; + case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: + case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: + if(csbk instanceof ChannelGrant) + { + ChannelGrant tgGrant = (ChannelGrant)csbk; + DMRChannel channel = tgGrant.getChannel(); + if(hasTrafficChannelManager()) + { + mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + } + processCallDetection(tgGrant.getChannel(), tgGrant.getIdentifiers(), tgGrant.getTimestamp(), DecodeEventType.CALL_GROUP); + } + 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()) + { + mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + } + processCallDetection(channelGrant.getChannel(), channelGrant.getIdentifiers(), + channelGrant.getTimestamp(), DecodeEventType.CALL_UNIT_TO_UNIT); + } + break; + case MOTOROLA_CAPMAX_ALOHA: + if(csbk instanceof CapacityMaxAloha) + { + CapacityMaxAloha cmAloha = (CapacityMaxAloha)csbk; + + if(cmAloha.hasRadioIdentifier()) + { + DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .eventDescription(DecodeEventType.RESPONSE.name()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .details("Aloha Acknowledge") + .build(); + + broadcast(ackEvent); + + resetState(); + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT: + if(csbk instanceof ConnectPlusDataChannelGrant) + { + ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk; + if(hasTrafficChannelManager()) + { + mTrafficChannelManager.processChannelGrant(cpdcg.getChannel(), + getMergedIdentifierCollection(csbk.getIdentifiers()), csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + } + processCallDetection(cpdcg.getChannel(), cpdcg.getIdentifiers(), cpdcg.getTimestamp(), + DecodeEventType.DATA_CALL); + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_REGISTRATION_REQUEST: + DecodeEvent event = DMRDecodeEvent.builder(csbk.getTimestamp()) + .details("Registration Request") + .eventDescription(DecodeEventType.REGISTER.toString()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .build(); + broadcast(event); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE: + DecodeEvent regRespEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .details("Registration Response") + .eventDescription(DecodeEventType.REGISTER.toString()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .build(); + broadcast(regRespEvent); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: + if(csbk instanceof ConnectPlusVoiceChannelUser) + { + DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel(); + if(hasTrafficChannelManager()) + { + mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + } + processCallDetection(channel, csbk.getIdentifiers(), csbk.getTimestamp(), + DecodeEventType.CALL_GROUP); + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION: + DecodeEvent affiliateEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .details("TALKGROUP AFFILIATION") + .eventDescription(DecodeEventType.AFFILIATE.toString()) + .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) + .timeslot(csbk.getTimeslot()) + .build(); + broadcast(affiliateEvent); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case STANDARD_MOVE_TSCC: + + + default: + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); + break; + } + } + + /** + * Creates a copy of the current identifier collection, removes any USER class identifiers and loads the identifiers + * argument values into the collection. + * @param identifiers to load into the collection. + * @return copy identifier collection. + */ + private IdentifierCollection getMergedIdentifierCollection(List identifiers) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(identifiers); + return mic; + } + + /** + * Process Call Detections. + * + * Note: once full support for Connect+ trunk tracking is implemented with the DMRTrafficChannelManager, this will + * have to be modified to detect if the trunked channel was allocated or if the call was simply detected. This + * would be contingent on having a map of Logical Slot Numbers to frequency for the traffic channel manager to make + * an allocation. + * + * @param channel for the call event + * @param identifiers for the call event + * @param timestamp of the event or update + */ + private void processCallDetection(DMRChannel channel, List identifiers, long timestamp, + DecodeEventType eventType) + { + //Check to see if there is a current call event to see if the detected call event is actually for this timeslot + //and we can then identify the LSN for this timeslot + if(mCurrentFrequency == 0 && mCurrentCallEvent != null && + isSameCall(mCurrentCallEvent.getIdentifierCollection(), identifiers)) + { + mCurrentFrequency = channel.getDownlinkFrequency(); + + if(mNetworkConfigurationMonitor != null) + { + mNetworkConfigurationMonitor.setCurrentChannel(channel); + } + } + + DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(event == null) + { + event = DMRDecodeEvent.builder(timestamp) + .timeslot(getTimeslot()) + .identifiers(getMergedIdentifierCollection(identifiers)) + .eventDescription(eventType.toString()) + .build(); +// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + else + { + if(event.getIdentifierCollection() != null && + isSameCall(event.getIdentifierCollection(), identifiers) && + FastMath.abs(timestamp - event.getTimeStart()) < MAX_VALID_CALL_DURATION_MS) + { + event.update(timestamp); + } + else + { + event = DMRDecodeEvent.builder(timestamp) + .timeslot(getTimeslot()) + .identifiers(getMergedIdentifierCollection(identifiers)) + .eventDescription(DecodeEventType.CALL_DETECT.toString()) + .build(); +// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + } + + //Only broadcast the call detect event if it doesn't match the current logical slot number +// if(mCurrentLSN == null || mCurrentLSN != channel.getLogicalSlotNumber()) +// { +// broadcast(event); +// } + } + + /** + * 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 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 + */ + private void processLinkControl(LCMessage message, boolean isTerminator) + { + switch(message.getOpcode()) + { + case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION: + if(message instanceof CapacityPlusRestChannel) + { + updateRestChannel(((CapacityPlusRestChannel)message).getRestChannel()); + } + break; + case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER: + if(message instanceof CapacityPlusGroupVoiceChannelUser) + { + CapacityPlusGroupVoiceChannelUser cpgvcu = (CapacityPlusGroupVoiceChannelUser)message; + + //This is the current channel - what do we do with the voice channel number? +// updateRestChannel(cpgvcu.getVoiceChannel()); + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(cpgvcu.getTalkgroup()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + ServiceOptions serviceOptions = cpgvcu.getServiceOptions(); + updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED : + DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp()); + } + } + break; + case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER: + if(message instanceof CapacityPlusWideAreaVoiceChannelUser) + { + CapacityPlusWideAreaVoiceChannelUser cpuo4 = (CapacityPlusWideAreaVoiceChannelUser)message; + + updateRestChannel(cpuo4.getRestChannel()); + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(cpuo4.getTalkgroup()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + } + } + break; + case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER: + if(message instanceof HyteraGroupVoiceChannelUser) + { + HyteraGroupVoiceChannelUser hgvcu = (HyteraGroupVoiceChannelUser)message; + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(hgvcu.getTalkgroup()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + ServiceOptions serviceOptions = hgvcu.getServiceOptions(); + updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED : + DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp()); + + } + } + break; + case FULL_HYTERA_TERMINATOR: + case FULL_STANDARD_TERMINATOR_DATA: + getIdentifierCollection().update(message.getIdentifiers()); + break; + case FULL_HYTERA_UNIT_TO_UNIT_VOICE_CHANNEL_USER: + if(message instanceof HyteraUnitToUnitVoiceChannelUser) + { + HyteraUnitToUnitVoiceChannelUser huuvcu = (HyteraUnitToUnitVoiceChannelUser)message; + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(huuvcu.getTargetRadio()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + ServiceOptions serviceOptions = huuvcu.getServiceOptions(); + updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : + DecodeEventType.CALL_UNIT_TO_UNIT, serviceOptions.toString(), message.getTimestamp()); + } + } + break; + case FULL_STANDARD_GROUP_VOICE_CHANNEL_USER: + if(message instanceof GroupVoiceChannelUser) + { + GroupVoiceChannelUser gvcu = (GroupVoiceChannelUser)message; + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(gvcu.getTalkgroup()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + ServiceOptions serviceOptions = gvcu.getServiceOptions(); + updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED : + DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp()); + } + } + break; + case FULL_STANDARD_UNIT_TO_UNIT_VOICE_CHANNEL_USER: + if(message instanceof UnitToUnitVoiceChannelUser) + { + UnitToUnitVoiceChannelUser uuvcu = (UnitToUnitVoiceChannelUser)message; + + if(isTerminator) + { + getIdentifierCollection().remove(Role.FROM); + getIdentifierCollection().update(uuvcu.getTargetRadio()); + } + else + { + getIdentifierCollection().update(message.getIdentifiers()); + ServiceOptions serviceOptions = uuvcu.getServiceOptions(); + updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : + DecodeEventType.CALL_UNIT_TO_UNIT, serviceOptions.toString(), message.getTimestamp()); + } + } + break; + case FULL_STANDARD_GPS_INFO: + if(message instanceof GPSInformation) + { + GPSInformation gpsInformation = (GPSInformation)message; + MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + ic.update(gpsInformation.getGPSLocation()); + + DecodeEvent gpsEvent = DMRDecodeEvent.builder(message.getTimestamp()) + .eventDescription(DecodeEventType.GPS.name()) + .identifiers(ic) + .timeslot(message.getTimeslot()) + .details("LOCATION:" + gpsInformation.getGPSLocation()) + .build(); + + broadcast(gpsEvent); + } + break; + } + } + + /** + * Updates or creates a current call event. + * + * @param type of call that will be used as an event description + * @param details of the call (optional) + * @param timestamp of the message indicating a call or continuation + */ + private void updateCurrentCall(DecodeEventType type, String details, long timestamp) + { + Event event = (mCurrentCallEvent == null ? Event.START : Event.CONTINUATION); + + if(mCurrentCallEvent == null) + { + mCurrentCallEvent = DMRDecodeEvent.builder(timestamp) + .channel(getCurrentChannel()) + .eventDescription(type.toString()) + .details(details) + .identifiers(getIdentifierCollection().copyOf()) + .build(); + + broadcast(mCurrentCallEvent); + } + else + { + if(type != DecodeEventType.CALL) + { + mCurrentCallEvent.setEventDescription(type.toString()); + mCurrentCallEvent.setDetails(details); + } + + mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf()); + mCurrentCallEvent.end(timestamp); + broadcast(mCurrentCallEvent); + } + + if(type == DecodeEventType.CALL_GROUP_ENCRYPTED || type == DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED) + { + broadcast(new DecoderStateEvent(this, event, State.ENCRYPTED, getTimeslot())); + } + else + { + broadcast(new DecoderStateEvent(this, event, State.CALL, getTimeslot())); + } + } + + /** + * Ends/closes the current call event. + * + * @param timestamp of the message that indicates the event has ended. + */ + private void closeCurrentCallEvent(long timestamp) + { + if(mCurrentCallEvent != null) + { + mCurrentCallEvent.end(timestamp); + broadcast(mCurrentCallEvent); + mCurrentCallEvent = null; + } + } + + @Override + public String getActivitySummary() + { + if(mNetworkConfigurationMonitor != null) + { + return mNetworkConfigurationMonitor.getActivitySummary(); + } + + return ""; + } + + @Override + public void receiveDecoderStateEvent(DecoderStateEvent event) + { + switch(event.getEvent()) + { + case REQUEST_RESET: + resetState(); + if(mNetworkConfigurationMonitor != null) + { + mNetworkConfigurationMonitor.reset(); + } + break; + case NOTIFICATION_SOURCE_FREQUENCY: + mCurrentFrequency = event.getFrequency(); + if(hasTrafficChannelManager()) + { + mTrafficChannelManager.setCurrentControlFrequency(mCurrentFrequency); + } + break; + default: + break; + } + } + + @Override + public void start() + { + //Change the default (45-second) traffic channel timeout to 1 second + if(mChannel.isTrafficChannel()) + { + broadcast(new ChangeChannelTimeoutEvent(this, ChannelType.TRAFFIC, 1000, getTimeslot())); + } + } + + @Override + public void init() + { + } + + @Override + public void stop() + { + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java new file mode 100644 index 00000000..32276186 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java @@ -0,0 +1,429 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.ISyncDetectListener; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.SyncLossMessage; +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.DMRMessage; +import io.github.dsheirer.module.decode.dmr.message.DMRMessageFactory; +import io.github.dsheirer.protocol.Protocol; +import io.github.dsheirer.record.binary.BinaryReader; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableByteBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Consumer; + +/** + * DMR Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors + * and issue phase corrections. + */ +public class DMRMessageFramer implements Listener, IDMRBurstDetectListener +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRMessageFramer.class); + + private static final double DMR_BIT_RATE = 9600.0; + + /** + * Provides sync detection and burst framing + */ + private DMRBurstFramer mBurstFramer; + + /** + * External listener for messages produced by this framer + */ + private Listener mMessageListener; + + /** + * External listener for sync state information + */ + private ISyncDetectListener mSyncDetectListener; + + /** + * Tracks current time for use with framed bursts/messages. This value is updated externally from sample buffer + * timestamps and internally as each framed message is received or sync loss is processed. + */ + private long mCurrentTime = System.currentTimeMillis(); + + /** + * Constructs an instance + * + * @param phaseLockedLoop to receive PLL phase lock error corrections that are identified by the DMRBurstFramer + * when known phase misaligned sync patterns are detected. + */ + public DMRMessageFramer(IPhaseLockedLoop phaseLockedLoop) + { + mBurstFramer = new DMRBurstFramer(this, phaseLockedLoop); + } + + /** + * Constructs an instance. + */ + public DMRMessageFramer() + { + this(null); + } + + /** + * Registers a sync detect listener to be notified each time a sync pattern and NID are detected. + */ + public void setSyncDetectListener(ISyncDetectListener syncDetectListener) + { + mSyncDetectListener = syncDetectListener; + } + + /** + * Current timestamp or timestamp of incoming message buffers that is continuously updated to as + * close as possible to the bits processed for the expected baud rate. + * + * @return current time + */ + private long getTimestamp() + { + return mCurrentTime; + } + + /** + * Sets the current time. This should be invoked by an incoming message buffer stream. + * + * @param currentTime + */ + public void setCurrentTime(long currentTime) + { + mCurrentTime = currentTime; + } + + /** + * Updates the current timestamp based on the number of bits processed versus the bit rate per second + * in order to keep an accurate running timestamp to use for timestamped message creation. + * + * @param bitsProcessed thus far + */ + private void updateBitsProcessed(int bitsProcessed) + { + if(bitsProcessed > 0) + { + mCurrentTime += (long)((double)bitsProcessed / DMR_BIT_RATE * 1000.0); + } + } + + /** + * Registers the listener for messages produced by this message framer + * + * @param messageListener to receive framed and decoded messages + */ + public void setListener(Listener messageListener) + { + mMessageListener = messageListener; + } + + /** + * Primary method for streaming decoded symbol dibits for message framing. + * + * @param dibit to process + */ + @Override + public void receive(Dibit dibit) + { + mBurstFramer.receive(dibit); + } + + /** + * Primary method for streaming decoded symbol byte arrays. + * + * @param buffer to process into a stream of dibits for processing. + */ + public void receive(ReusableByteBuffer buffer) + { + //Updates current timestamp to the timestamp from the incoming buffer + setCurrentTime(buffer.getTimestamp()); + + for(byte value : buffer.getBytes()) + { + for(int x = 0; x <= 3; x++) + { + receive(Dibit.parse(value, x)); + } + } + + buffer.decrementUserCount(); + } + + /** + * DMR burst detection processing. Processes DMR burst/messages from external burst framer/producer. + * + * @param message bits for the burst/message and any bit errors detected in the sync pattern + * @param syncPattern that was detected for the burst + */ + @Override + public void burstDetected(CorrectedBinaryMessage message, DMRSyncPattern syncPattern, int timeslot) + { + //Each burst is 288 bits + updateBitsProcessed(288); + + //This allows the frequency error monitor to be notified when we're in sync + if(mSyncDetectListener != null) + { + mSyncDetectListener.syncDetected(0); + } + + CACH cach = null; + + if(syncPattern.hasCACH()) + { + cach = CACH.getCACH(message); + } + + if(mMessageListener != null) + { + DMRMessage dmrMessage = DMRMessageFactory.create(syncPattern, message, cach, getTimestamp(), timeslot); + + if(dmrMessage != null) + { + mMessageListener.receive(dmrMessage); + } + } + } + + /** + * Processes a sync loss to track how many bits were processed and update message listeners. + * + * @param bitsProcessed since the last sync detect + */ + @Override + public void syncLost(int bitsProcessed, int timeslot) + { + updateBitsProcessed(bitsProcessed); + + dispatchSyncLoss(bitsProcessed, timeslot); + + if(mSyncDetectListener != null) + { + mSyncDetectListener.syncLost(bitsProcessed); + } + } + + /** + * Creates a sync loss message and dispatches it to the message listener + * + * @param bitsProcessed without a sync detection + * @param timeslot that lost the sync + */ + private void dispatchSyncLoss(int bitsProcessed, int timeslot) + { + if(bitsProcessed > 0 && mMessageListener != null) + { + mMessageListener.receive(new SyncLossMessage(getTimestamp(), bitsProcessed, Protocol.DMR, timeslot)); + } + } + + public static class MessageListener implements Listener + { + private boolean mHasDMRData = false; + public int mTS0Count = 0; + public int mTS1Count = 0; + + @Override + public void receive(IMessage message) + { + if(message instanceof DMRBurst && ((DMRBurst)message).hasCACH()) + { + CACH cach = ((DMRBurst)message).getCACH(); + mLog.info(cach.toString() + " TS:" + message.getTimeslot() + " " + message.toString()); + } + else + { + mLog.info(" TS:" + message.getTimeslot() + " " + message.toString()); + } + +// if(message instanceof UDTHeader) +// { +// mLog.info("TS:" + message.getTimeslot() + " " + message.toString()); +// } + +// if(message instanceof UnknownCSBKMessage && message.isValid()) +// { +// mLog.info("TS:" + ((DMRMessage)message).getTimeslot() + " " + message.toString()); +// } + } + + public boolean hasData() + { + return mHasDMRData; + } + + public void reset() + { + mHasDMRData = true; + } + } + + public static void main(String[] args) + { + String path = "/media/denny/Lexar/Recordings/DMR/"; + + //Con+ Traffic + Voice +// String file = path + "20200513_143340_9600BPS_DMR_SaiaNet_Onondaga_Control.bits"; //Enh GPS Revert Window Annce +// String file = path + "20200514_062135_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Neighbor only +// String file = path + "20200514_063507_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 2579 +// String file = path + "20200514_064224_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 5056035 +// String file = path + "20200514_131623_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Grant: 5074193 +// String file = path + "20200514_133947_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; //<<<<<<<------ Basic Encryption +// String file = path + "20200514_142249_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; +// String file = path + "20200514_144534_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Con+ Control w/GPS Window Announce + + //Con+ Multi-Site with Network Frequency Map - This file is really General Motors Con+ - Tarrant County site +// String file = path + "20200716_235004_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; + + //Cap-Max Tier III CC +// String file = path + "20200710_053632_9600BPS_DMR_Niles_Radio_Coconino_Control.bits"; + + //Cap+ Multi-Site 1 - Traffic LCN 2 +// String file = path + "20200716_210133_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits"; +// String file = path + "20200716_212309_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits"; + + //Cap+ Multi-Site Enhanced GPS Channel +// String file = path + "20200714_224018_9600BPS_DMR_Farmers_Electric_Cooperative_Hunt_LCN_3.bits"; //This may have PLL mis-align issues + + //Hytera Short Data = Proprietary (encrypted) +// String file = path + "20200716_222839_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits"; //Hytera short data packets +// String file = path + "20200716_223551_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits"; + + //Motorola GPS Revert channel +// String file = path + "20200730_085433_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits"; + + //Hytera XPT (pseudo trunk) beaconing and voice +// String file = path + "20200729_161450_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //PLL lock oscillating +// String file = path + "20200729_162200_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; +// String file = path + "Hytera XPT/20200816_161126_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //analzing this <<<<<<<<<<<<<<<<<<<<<<<<<< + + //Hytera Tier III - CSBKO 40 +// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; +// String file = path + "20200812_061410_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; +// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; //MBC-channel grant + + //UDT Header And Blocks +// String file = path + "20200730_090634_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits"; + + //Cap+ BP Scrambling +// String file = path + "20200829_065610_9600BPS_DMR_Albany_Medical_Center_Albany_LCN_3.bits"; + + String file = "/home/denny/SDRTrunk/recordings/20200927_054837_9600BPS_DMR_Blair_Communications_(Capacity_Plus)_Dallas_Control_9.bits"; + + + + MessageListener listener = new MessageListener(); + DecodeConfigDMR config = new DecodeConfigDMR(); + + boolean multi = false; + + if(multi) + { + try + { + DirectoryStream stream = Files.newDirectoryStream(Paths.get(path), "*.bits"); + + stream.forEach(new Consumer() + { + @Override + public void accept(Path path) + { + mLog.info("Processing: " + path.toString()); + DMRMessageFramer messageFramer = new DMRMessageFramer(null); + DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); + messageFramer.setListener(messageProcessor); + messageProcessor.setMessageListener(listener); + + try(BinaryReader reader = new BinaryReader(path, 200)) + { + while(reader.hasNext()) + { + ReusableByteBuffer buffer = reader.next(); + messageFramer.receive(buffer); + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } + + if(!listener.hasData()) + { +// mLog.info("Has Data: " + listener.hasData() + " File:" + path.toString()); +// try +// { +// Files.delete(path); +// } +// catch(IOException ioe) +// { +// ioe.printStackTrace(); +// } + } + + listener.reset(); + + System.out.println("TS0 VOICE:" + listener.mTS0Count + " TS1 VOICE:" + listener.mTS1Count); + listener.mTS0Count = 0; + listener.mTS1Count = 0; + } + }); + } + catch(IOException ioe) + { + ioe.printStackTrace(); + } + } + else + { + DMRMessageFramer messageFramer = new DMRMessageFramer(null); + DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); + messageFramer.setListener(messageProcessor); + messageProcessor.setMessageListener(listener); + + try(BinaryReader reader = new BinaryReader(Path.of(file), 200)) + { + while(reader.hasNext()) + { + ReusableByteBuffer buffer = reader.next(); + messageFramer.receive(buffer); + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } + + System.out.println("TS0 VOICE:" + listener.mTS0Count + " TS1 VOICE:" + listener.mTS1Count); + listener.mTS0Count = 0; + listener.mTS1Count = 0; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java new file mode 100644 index 00000000..2eab6b6e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java @@ -0,0 +1,218 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.message.IMessage; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.DMRBurst; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble; +import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FLCAssembler; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.SLCAssembler; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCAssembler; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler; +import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Processes DMR messages and performs re-assembly of link control fragments + */ +public class DMRMessageProcessor implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class); + + private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1); + private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2); + private MBCAssembler mMBCAssembler = new MBCAssembler(); + private PacketSequenceAssembler mPacketSequenceAssembler; + private SLCAssembler mSLCAssembler = new SLCAssembler(); + private Listener mMessageListener; + private Map mTimeslotFrequencyMap = new TreeMap<>(); + + /** + * Constructs an instance + */ + public DMRMessageProcessor(DecodeConfigDMR config) + { + for(TimeslotFrequency timeslotFrequency: config.getTimeslotMap()) + { + mTimeslotFrequencyMap.put(timeslotFrequency.getNumber(), timeslotFrequency); + } + + mPacketSequenceAssembler = new PacketSequenceAssembler(); + } + + /** + * Primary message processing + */ + @Override + public void receive(IMessage message) + { + //Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings + if(message instanceof ITimeslotFrequencyReceiver) + { + ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message; + int[] lsns = receiver.getLogicalTimeslotNumbers(); + + List timeslotFrequencies = new ArrayList<>(); + + for(int lsn: lsns) + { + if(mTimeslotFrequencyMap.containsKey(lsn)) + { + timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn)); + } + } + + if(!timeslotFrequencies.isEmpty()) + { + receiver.apply(timeslotFrequencies); + } + } + + //Extract the Full Link Control message fragment from the Voice with embedded signalling message + if(message instanceof VoiceEMBMessage) + { + VoiceEMBMessage voice = (VoiceEMBMessage)message; + + if(message.getTimeslot() == 1) + { + FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(), + voice.getFLCFragment(), message.getTimestamp()); + receive(flco); + } + else + { + FullLCMessage flco = mFLCAssemblerTimeslot2.process(voice.getEMB().getLCSS(), + voice.getFLCFragment(), message.getTimestamp()); + receive(flco); + } + + if(voice.hasCACH()) + { + CACH cach = voice.getCACH(); + ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp()); + receive(slco); + } + } + //Extract the Short Link Control message fragment from the DMR burst message when it has one + else if(message instanceof DMRBurst) + { + DMRBurst dmrBurst = (DMRBurst)message; + + if(dmrBurst.hasCACH()) + { + CACH cach = dmrBurst.getCACH(); + ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp()); + receive(slco); + } + + //Multi-Block CSBK Reassembly + if(message instanceof MBCHeader) + { + mMBCAssembler.process((MBCHeader)message); + } + else if(message instanceof MBCContinuationBlock) + { + //Returns either a fully reassembled MultiCSBK or null + receive(mMBCAssembler.process((MBCContinuationBlock)message)); + } + else + { + mMBCAssembler.reset(message.getTimeslot()); + } + + //Packet Sequence Message Assembly ... + if(message instanceof Preamble) + { + mPacketSequenceAssembler.process((Preamble)message); + } + else if(message instanceof PacketSequenceHeader) + { + mPacketSequenceAssembler.process((PacketSequenceHeader)message); + } + else if(message instanceof ProprietaryDataHeader) + { + mPacketSequenceAssembler.process(((ProprietaryDataHeader)message)); + } + else if(message instanceof DataBlock) + { + mPacketSequenceAssembler.process((DataBlock)message); + } + } + + //Now that the message has been (potentially) enriched, dispatch it to the modules + dispatch(message); + } + + /** + * Dispatches the non-null message to the registered listener + */ + private void dispatch(IMessage message) + { + if(mMessageListener != null && message != null) + { + mMessageListener.receive(message); + } + } + + /** + * Prepares for disposal + */ + public void dispose() + { + mMessageListener = null; + } + + /** + * Registers the listener to receive messages from this processor + */ + public void setMessageListener(Listener listener) + { + mMessageListener = listener; + mPacketSequenceAssembler.setMessageListener(listener); + } + + /** + * Removes the listener from receiving messages from this processor + */ + public void removeMessageListener() + { + mMessageListener = null; + mPacketSequenceAssembler.setMessageListener(null); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java new file mode 100644 index 00000000..6280bd62 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java @@ -0,0 +1,369 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.identifier.site.SiteIdentifier; +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.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; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusNeighborReport; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AdjacentSiteInformation; +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.type.Model; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tracks the network configuration details of a DMR network from the broadcast messages + */ +public class DMRNetworkConfigurationMonitor +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRNetworkConfigurationMonitor.class); + private static final String BRAND_HYTERA = "Hytera"; + private static final String BRAND_MOTOROLA_CAPACITY_PLUS = "Motorola Capacity+"; + private static final String BRAND_MOTOROLA_CONNECT_PLUS = "Motorola Connect+"; + private static final String BRAND_STANDARD = "Standard"; + 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 List mNeighborSites = new ArrayList<>(); + private Map mTier3NeighborSites = new HashMap<>(); + private Map mObservedChannelMap = new HashMap<>(); + private DMRNetwork mDMRNetwork; + private DMRSite mDMRSite; + private Model mTier3Model; + private String mBrand; + private Integer mColorCode; + private DMRChannel mCurrentChannel; + private Channel mChannel; + + /** + * Constructs an instance + * @param channel configuration + */ + public DMRNetworkConfigurationMonitor(Channel channel) + { + mChannel = channel; + } + + /** + * Sets the current channel + */ + public void setCurrentChannel(DMRChannel channel) + { + mCurrentChannel = channel; + } + + /** + * Process a DMR message + * @param message to process that has already been checked for isValid() + */ + public void process(DMRMessage message) + { + if(message instanceof CSBKMessage) + { + process((CSBKMessage)message); + } + else if(message instanceof LCMessage) + { + process((LCMessage)message); + } + } + + /** + * Processes link control messages + */ + public void process(LCMessage linkControl) + { + switch(linkControl.getOpcode()) + { + case SHORT_CONNECT_PLUS_CONTROL_CHANNEL: + if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusControlChannel) + { + ConnectPlusControlChannel cpcc = (ConnectPlusControlChannel)linkControl; + mDMRNetwork = cpcc.getNetwork(); + mDMRSite = cpcc.getSite(); + } + if(mBrand == null) + { + mBrand = BRAND_MOTOROLA_CONNECT_PLUS; + } + break; + case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL: + if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusTrafficChannel) + { + ConnectPlusTrafficChannel cptc = (ConnectPlusTrafficChannel)linkControl; + mDMRNetwork = cptc.getNetwork(); + mDMRSite = cptc.getSite(); + } + if(mBrand == null) + { + mBrand = BRAND_MOTOROLA_CONNECT_PLUS; + } + break; + } + } + + /** + * Processes Control Signalling Blocks (CSBK) + */ + public void process(CSBKMessage csbk) + { + switch(csbk.getOpcode()) + { + case STANDARD_ALOHA: + if(csbk instanceof Aloha) + { + if(mDMRNetwork == null || mDMRSite == null) + { + SystemIdentityCode sic = ((Aloha)csbk).getSystemIdentityCode(); + + if(mDMRNetwork == null) + { + mDMRNetwork = sic.getNetwork(); + } + if(mDMRSite == null) + { + mDMRSite = sic.getSite(); + } + if(mTier3Model == null) + { + mTier3Model = sic.getModel(); + } + } + + if(mBrand == null) + { + mBrand = BRAND_TIER_3_TRUNKING; + } + } + break; + case HYTERA_08_ANNOUNCEMENT: + case HYTERA_68_ANNOUNCEMENT: + if(csbk instanceof HyteraAnnouncement) + { + HyteraAnnouncement ha = (HyteraAnnouncement)csbk; + + if(mBrand == null) + { + mBrand = BRAND_HYTERA_TIER_3_TRUNKING; + } + + if(mDMRNetwork == null) + { + mDMRNetwork = ha.getSystemIdentityCode().getNetwork(); + } + if(mDMRSite == null) + { + mDMRSite = ha.getSystemIdentityCode().getSite(); + } + if(mTier3Model == null) + { + mTier3Model = ha.getSystemIdentityCode().getModel(); + } + + if(mBrand == null || mBrand != BRAND_HYTERA_TIER_3_TRUNKING) + { + mBrand = BRAND_HYTERA_TIER_3_TRUNKING; + } + } + if(csbk instanceof HyteraAdjacentSiteInformation) + { + HyteraAdjacentSiteInformation hasi = (HyteraAdjacentSiteInformation)csbk; + + int site = hasi.getNeighborSystemIdentityCode().getSite().getValue(); + + if(!mTier3NeighborSites.containsKey(site)) + { + mTier3NeighborSites.put(site, hasi); + } + } + break; + case MOTOROLA_CAPMAX_ALOHA: + if(csbk instanceof CapacityMaxAloha) + { + if(mDMRNetwork == null || mDMRSite == null) + { + SystemIdentityCode sic = ((CapacityMaxAloha)csbk).getSystemIdentityCode(); + + if(mDMRNetwork == null) + { + mDMRNetwork = sic.getNetwork(); + } + if(mDMRSite == null) + { + mDMRSite = sic.getSite(); + } + if(mTier3Model == null) + { + mTier3Model = sic.getModel(); + } + } + + if(mBrand == null || mBrand != BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING) + { + mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING; + } + } + 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; + } + break; + case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: + if(csbk instanceof ConnectPlusVoiceChannelUser) + { + DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel(); + addDmrChannel(channel); + } + if(mBrand == null) + { + mBrand = BRAND_MOTOROLA_CONNECT_PLUS; + } + break; + } + + if(mColorCode == null) + { + mColorCode = csbk.getSlotType().getColorCode(); + } + } + + /** + * Adds the DMR channel to the observed channel map + */ + private void addDmrChannel(DMRChannel dmrChannel) + { + mObservedChannelMap.put(dmrChannel.getLogicalSlotNumber(), 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)); + + if(mDMRNetwork != null) + { + sb.append("\nNetwork:").append(mDMRNetwork); + } + + if(mDMRSite != null) + { + sb.append("\nSite:").append(mDMRSite); + } + + if(mTier3Model != null) + { + sb.append("\nNetwork Model").append(mTier3Model); + } + + //Observed DMR Channels + if(!mObservedChannelMap.isEmpty()) + { + sb.append("\nObserved Logical Slot Numbers (LSN):"); + + List lsns = new ArrayList<>(mObservedChannelMap.keySet()); + Collections.sort(lsns); + + for(Integer lsn: lsns) + { + DMRChannel channel = mObservedChannelMap.get(lsn); + + if(channel != null) + { + sb.append("\n\t").append(channel); + + double frequency = channel.getDownlinkFrequency(); + + if(frequency != 0) + { + frequency /= 1E6d; + } + sb.append(" ").append(frequency).append(" MHz"); + } + } + } + + if(!mNeighborSites.isEmpty()) + { + sb.append("\nNeighbor Sites\n"); + + for(SiteIdentifier neighbor: mNeighborSites) + { + sb.append("\tNeighbor: ").append(neighbor).append("\n"); + } + + for(Map.Entry neighbor: mTier3NeighborSites.entrySet()) + { + AdjacentSiteInformation site = neighbor.getValue(); + 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"); + } + } + + sb.append("\n\n"); + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java new file mode 100644 index 00000000..0d77a2b5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java @@ -0,0 +1,42 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.controller.channel.event.PreloadDataContent; + +/** + * Preload data container for DMRNetworkConfigurationMonitor. + * + * Note: this is used to transfer the monitor during a Capacity plus REST channel rotation operation. The monitor + * is passed from the channel that is converting from REST to a TRAFFIC channel and passed to the new STANDARD channel + * that represents the REST channel. + */ +public class DMRNetworkConfigurationPreloadData extends PreloadDataContent +{ + /** + * Constructs an instance + * + * @param data to preload + */ + public DMRNetworkConfigurationPreloadData(DMRNetworkConfigurationMonitor data) + { + super(data); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java new file mode 100644 index 00000000..4bccc8f5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java @@ -0,0 +1,178 @@ +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.QPSKCarrierLock; +import org.apache.commons.lang3.Validate; + +/** + * Detector for DMR sync patterns that also includes support for detecting PLL phase lock errors. There are + * two primary use cases for this class: + * + * The first is to detect a sync pattern from a stream of dibits that are processed via the add(dibit) method. After + * each dibit is added, the sync pattern can be inspected by either the hasSync() method or the getSyncPattern() + * method. The maxStreamBitErrors construction parameter specifies how many bit errors can be present when matching + * patterns to the dibit stream. + * + * The second use case is to set a candidate sync pattern value using the setCurrentSyncValue() method. You can then + * use the hasSync() and getSyncPatter() methods to determine if the set value matches a valid sync pattern. The + * maxExplicitBitErrors specifies how many bit errors can be present when matching patterns to the explicitly set + * sync value. + * + * Note: sync pattern matching maximum bit errors uses a hard-coded value for PLL misalignment sync detection since + * this threshold should be relatively high to avoid false triggering and once detected, a PLL should be immediately + * commanded to correct the issue. + */ +public class DMRSyncDetector +{ + private static long SYNC_MASK = 0xFFFFFFFFFFFFl; + private static final int MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT = 1; + private int mMaxStreamBitErrors; + private int mMaxExplicitBitErrors; + private long mCurrentSyncValue; + private long mErrorPattern; + private int mPatternMatchBitErrorCount; + private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN; + private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL; + + /** + * Constructs an instance + * @param maxStreamBitErrors allowed for matching sync patterns when using the dibit streaming use case. + * @param maxExplicitBitErrors allows for matching sync patterns when using the explicitly set sync value use case. + */ + public DMRSyncDetector(int maxStreamBitErrors, int maxExplicitBitErrors) + { + Validate.inclusiveBetween(0, 24, maxStreamBitErrors, + "Max (allowable) stream bit errors for sync match must be between 0 and 24"); + Validate.inclusiveBetween(0, 24, maxExplicitBitErrors, + "Max (allowable) explicit bit errors for sync match must be between 0 and 24"); + mMaxStreamBitErrors = maxStreamBitErrors; + mMaxExplicitBitErrors = maxExplicitBitErrors; + } + + /** + * Sync pattern for the current sync value. + */ + public DMRSyncPattern getSyncPattern() + { + return mSyncPattern; + } + + /** + * Indicates if there is a sync pattern in the currently set sync value that was either explicitly set or was + * aggregated from a stream of dibits. When this method returns true, the sync pattern can be accessed via the + * getSyncPattern() method. + */ + public boolean hasSync() + { + return getSyncPattern() != DMRSyncPattern.UNKNOWN; + } + + /** + * Carrier lock for the QPSK PLL to detect when the PLL has locked to the signal at +/- 90 degrees or 180 degrees. + * @return carrier lock value. + */ + public QPSKCarrierLock getCarrierLock() + { + return mCarrierLock; + } + + /** + * Indicates if the detected sync pattern has normal QPSK carrier lock. + */ + public boolean hasNormalCarrierLock() + { + return getCarrierLock() == QPSKCarrierLock.NORMAL; + } + + /** + * Number of bit errors that were present when matching the current sync value to the current sync pattern. + */ + public int getPatternMatchBitErrorCount() + { + return mPatternMatchBitErrorCount; + } + + /** + * Processes the streaming dibit by left shifting the current sync value and adding the dibit onto the end and then + * test for sync pattern match. + * @param dibit to process and test for sync pattern. + */ + public void add(Dibit dibit) + { + mCurrentSyncValue = Long.rotateLeft(mCurrentSyncValue, 2); + mCurrentSyncValue &= SYNC_MASK; + mCurrentSyncValue += dibit.getValue(); + checkSync(mMaxStreamBitErrors); + } + + /** + * Explicitly sets the argument as the current sync value and tests for sync pattern match. + * @param value to load as the current sync value and test + */ + public void setCurrentSyncValue(long value) + { + mCurrentSyncValue = value; + checkSync(mMaxExplicitBitErrors); + } + + /** + * Checks the current sync value against each of the sync patterns to determine if the value matches a pattern. + */ + private void checkSync(int maxBitErrors) + { + for(DMRSyncPattern pattern: DMRSyncPattern.SYNC_PATTERNS) + { + mErrorPattern = mCurrentSyncValue ^ pattern.getPattern(); + + if(mErrorPattern == 0) + { + mSyncPattern = pattern; + mCarrierLock = QPSKCarrierLock.NORMAL; + mPatternMatchBitErrorCount = 0; + return; + } + + mPatternMatchBitErrorCount = Long.bitCount(mErrorPattern); + + if(mPatternMatchBitErrorCount <= maxBitErrors) + { + mSyncPattern = pattern; + mCarrierLock = QPSKCarrierLock.NORMAL; + return; + } + + + //For PLL mis-aligned lock patterns, reduce the pattern bit error match threshold + mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getPlus90Pattern()); + + if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT) + { + mSyncPattern = pattern; + mCarrierLock = QPSKCarrierLock.PLUS_90; + return; + } + + mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getMinus90Pattern()); + + if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT) + { + mSyncPattern = pattern; + mCarrierLock = QPSKCarrierLock.MINUS_90; + return; + } + + mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getInvertedPattern()); + + if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT) + { + mSyncPattern = pattern; + mCarrierLock = QPSKCarrierLock.INVERTED; + return; + } + } + + mSyncPattern = DMRSyncPattern.UNKNOWN; + mCarrierLock = QPSKCarrierLock.NORMAL; + mPatternMatchBitErrorCount = 0; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java new file mode 100644 index 00000000..60d94792 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java @@ -0,0 +1,154 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr; + +import java.util.EnumSet; + +/** + * DMR burst sync patterns enumeration. In addition to the ETSI defined sync patterns, each enumeration entry also + * contains derived sync patterns that match decoded bit streams where a QPSK Phase-Locked Loop has locked to the signal + * misaligned by +/- 90 degrees, or 180 degrees. These derived patterns can be used to detect PLL misalignment for + * PLL correction purposes. + */ +public enum DMRSyncPattern +{ + BASE_STATION_DATA(0xDFF57D75DF5Dl, 0xBAAFEBEFBAFBl, 0x455014104504l, 0x200A828A20A2l, "BS DATA"), + BASE_STATION_VOICE(0x755FD7DF75F7l, 0xEFFABEBAEFAEl, 0x100541451051l, 0x8AA028208A08l, "BS VOICE A"), + + MOBILE_STATION_DATA(0xD5D7F77FD757l, 0xBFBEAEEABEFEl, 0x404151154101l, 0x2A28088028A8l, "MS DATA"), + MOBILE_STATION_VOICE(0x7F7D5DD57DFDl, 0xEAEBFBBFEBABl, 0x151404401454l, 0x8082A22A8202l, "MS VOICE A"), + DIRECT_MODE_DATA_TIMESLOT_1(0xF7FDD5DDFD55l, 0xAEABBFBBABFFl, 0x515440445400l, 0x08022A2202AAl, "DM DAT0"), + DIRECT_MODE_DATA_TIMESLOT_2(0xD7557F5FF7F5l, 0xBEFFEAFAAEAFl, 0x410015055150l, 0x28AA80A0080Al, "DM DAT1"), + DIRECT_MODE_VOICE_TIMESLOT_1(0x5D577F7757FFl, 0xFBFEEAEEFEAAl, 0x040115110155l, 0xA2A88088A800l, "DM VOX0"), + DIRECT_MODE_VOICE_TIMESLOT_2(0x7DFFD5F55D5Fl, 0xEBAABFAFFBFAl, 0x145540500405l, 0x82002A0AA2A0l, "DM VOX1"), + MOBILE_STATION_REVERSE_CHANNEL(0x77D55F7DFD77l, 0xEEBFFAEBABEEl, 0x114005145411l, 0x882AA0820288l, "MS RVRS"), + + RESERVED(0xDD7FF5D757DDl, 0xBBEAAFBEFEBBl, 0x441550410144l, 0x22800A28A822l, "RESERVED"), + + //These are used to identify the sync-less sub frames of a voice super frame + BS_VOICE_FRAME_B("BS VOICE B"), + BS_VOICE_FRAME_C("BS VOICE C"), + BS_VOICE_FRAME_D("BS VOICE D"), + BS_VOICE_FRAME_E("BS VOICE E"), + BS_VOICE_FRAME_F("BS VOICE F"), + + MS_VOICE_FRAME_B("MS VOICE B"), + MS_VOICE_FRAME_C("MS VOICE C"), + MS_VOICE_FRAME_D("MS VOICE D"), + MS_VOICE_FRAME_E("MS VOICE E"), + MS_VOICE_FRAME_F("MS VOICE F"), + + UNKNOWN("UNKNOWN"); + + private long mPattern; + private long mPlus90Pattern; + private long mMinus90Pattern; + private long mInvertedPattern; + private String mLabel; + + /** + * DMR Sync Patterns. See TS 102-361-1, paragraph 9.1.1 + */ + DMRSyncPattern(long pattern, long plus90Pattern, long minus90Pattern, long invertedPattern, String label) + { + mPattern = pattern; + mPlus90Pattern = plus90Pattern; + mMinus90Pattern = minus90Pattern; + mInvertedPattern = invertedPattern; + mLabel = label; + } + + /** + * Alternate constructor for syncs that don't have defined patterns. + */ + DMRSyncPattern(String label) + { + mLabel = label; + } + + //Valid sync patterns (excluding the sync-less voice patterns) + public static final EnumSet SYNC_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE, + MOBILE_STATION_DATA, MOBILE_STATION_VOICE, DIRECT_MODE_DATA_TIMESLOT_1, DIRECT_MODE_DATA_TIMESLOT_2, + DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2, MOBILE_STATION_REVERSE_CHANNEL); + + //Sync patterns containing a Common Announcement Channel (CACH) + private static final EnumSet CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE, + BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F); + + //Direct Mode Sync Patterns + private static final EnumSet DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1, + DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2); + + /** + * Pattern that represents the enum entry + */ + public long getPattern() + { + return mPattern; + } + + /** + * Pattern that represents the PLL locking to the carrier misaligned by plus 90 degrees. + */ + public long getPlus90Pattern() + { + return mPlus90Pattern; + } + + /** + * Pattern that represents the PLL locking to the carrier misaligned by minus 90 degrees. + */ + public long getMinus90Pattern() + { + return mMinus90Pattern; + } + + /** + * Pattern that represents the PLL locking to the carrier misaligned by 180 degrees (inverted). + */ + public long getInvertedPattern() + { + return mInvertedPattern; + } + + /** + * Indicates if a message using this sync pattern contains a Common Announcement Channel (CACH). + */ + public boolean hasCACH() + { + return CACH_PATTERNS.contains(this); + } + + /** + * Indicates if this is a Direct Mode sync pattern + */ + public boolean isDirectMode() + { + return DIRECT_MODE_PATTERNS.contains(this); + } + + /** + * String representation of the sync pattern + */ + public String toString() + { + return mLabel; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java new file mode 100644 index 00000000..4a6bf168 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -0,0 +1,696 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ +package io.github.dsheirer.module.decode.dmr; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.controller.channel.Channel.ChannelType; +import io.github.dsheirer.controller.channel.ChannelConversionRequest; +import io.github.dsheirer.controller.channel.ChannelEvent; +import io.github.dsheirer.controller.channel.ChannelEvent.Event; +import io.github.dsheirer.controller.channel.IChannelEventListener; +import io.github.dsheirer.controller.channel.IChannelEventProvider; +import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.MessageHistoryPreloadData; +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.message.data.csbk.Opcode; +import io.github.dsheirer.module.decode.event.DecodeEvent; +import io.github.dsheirer.module.decode.event.DecodeEventHistory; +import io.github.dsheirer.module.decode.event.DecodeEventHistoryPreloadData; +import io.github.dsheirer.module.decode.event.DecodeEventHistoryRequest; +import io.github.dsheirer.module.decode.event.DecodeEventHistoryResponse; +import io.github.dsheirer.module.decode.event.DecodeEventType; +import io.github.dsheirer.module.decode.event.IDecodeEvent; +import io.github.dsheirer.module.decode.event.IDecodeEventProvider; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceType; +import io.github.dsheirer.source.config.SourceConfigTuner; +import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; +import io.github.dsheirer.source.tuner.channel.rotation.DisableChannelRotationMonitorRequest; +import io.github.dsheirer.source.tuner.channel.rotation.FrequencyLockChangeRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Monitors channel grant and channel grant update messages to allocate traffic channels to capture + * traffic channel activity. + * + * Creates and reuses a limited set of Channel instances each with a TRAFFIC channel type. Since each of + * the traffic channels will be using the same decoder type and configuration options, we reuse each of the + * channel instances to allow the ChannelProcessingManager to reuse a cached set of processing chains that + * are created as each channel is activated. + * + * Each traffic channel is activated by sending a ChannelEvent via the channel model. The channel processing + * manager receives the activation request. If successful, a processing chain is activated for the traffic + * channel. Otherwise, a channel event is broadcast indicating that the channel could not be activated. On + * teardown of an activated traffic channel, a channel event is broadcast to indicate the traffic channels + * is no longer active. + * + * This manager monitors channel events watching for events related to managed traffic channels. + */ +public class DMRTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener, + IChannelEventProvider +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRTrafficChannelManager.class); + public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; + public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED"; + + private Queue mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>(); + private List mManagedTrafficChannels; + + private Map mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>(); + private Map mLSNGrantEventMap = new ConcurrentHashMap<>(); + + private Listener mChannelEventListener; + private Listener mDecodeEventListener; + + private TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor(); + private Channel mParentChannel; + + //Used as temporary storage for message and decode event history during Cap+ REST channel rotation + private DecodeEventHistory mTransientDecodeEventHistory; + private List 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 + * traffic channel allocations. + * + * @param parentChannel that owns this traffic channel manager + */ + public DMRTrafficChannelManager(Channel parentChannel) + { + mParentChannel = parentChannel; + + if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigDMR) + { + mIgnoreDataCalls = ((DecodeConfigDMR)parentChannel.getDecodeConfiguration()).getIgnoreDataCalls(); + } + + createTrafficChannels(); + } + + /** + * 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. + */ + public void setCurrentControlFrequency(long currentControlFrequency) + { + mCurrentControlFrequency = currentControlFrequency; + } + + /** + * Creates up to the maximum number of traffic channels for use in allocating traffic channels. + * + * Note: this method uses lazy initialization and will only create the channels once. Subsequent calls will be ignored. + */ + private void createTrafficChannels() + { + if(mManagedTrafficChannels == null) + { + DecodeConfiguration decodeConfiguration = mParentChannel.getDecodeConfiguration(); + List trafficChannelList = new ArrayList<>(); + + if(decodeConfiguration instanceof DecodeConfigDMR) + { + DecodeConfigDMR decodeConfig = (DecodeConfigDMR)decodeConfiguration; + + int maxTrafficChannels = decodeConfig.getTrafficChannelPoolSize(); + + if(maxTrafficChannels > 0) + { + for(int x = 0; x < maxTrafficChannels; x++) + { + Channel trafficChannel = new Channel("TRAFFIC", ChannelType.TRAFFIC); + trafficChannel.setAliasListName(mParentChannel.getAliasListName()); + trafficChannel.setSystem(mParentChannel.getSystem()); + trafficChannel.setSite(mParentChannel.getSite()); + trafficChannel.setDecodeConfiguration(decodeConfig); + trafficChannel.setEventLogConfiguration(mParentChannel.getEventLogConfiguration()); + trafficChannel.setRecordConfiguration(mParentChannel.getRecordConfiguration()); + trafficChannelList.add(trafficChannel); + } + } + } + + mAvailableTrafficChannelQueue.addAll(trafficChannelList); + mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList); + } + } + + /** + * Used with Capacity Plus systems to convert the existing standard channel to a traffic channel and then recreate + * the original standard channel with the frequency specified for the new rest channel. + * @param channel + * @param currentFrequency of the standard channel + * @param restChannel to identify the new channel frequency to start + */ + 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) + { + SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration(); + + //Add the rest channel to the list of frequencies in the source configuration + if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency())) + { + originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency()); + + //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); + } + } + } + + /** + * Processes a decode event history response and temporarily stores the event history. + * + * Note: this is used for Cap+ REST channel rotation. + * + * @param response containing the current decode event history. + */ + @Subscribe + public void process(DecodeEventHistoryResponse response) + { + mTransientDecodeEventHistory = response.getDecodeEventHistory(); + } + + /** + * Processes a message history response and temporarily stores the history. + * + * Note: this is used for Cap+ REST channel rotation. + * + * @param response containing the current message history. + */ + @Subscribe + public void process(MessageHistoryResponse response) + { + mTransientMessageHistory = response.getMessages(); + } + + /** + * Broadcasts an initial or update decode event to any registered listener. + */ + public void broadcast(DecodeEvent decodeEvent) + { + if(mDecodeEventListener != null) + { + mDecodeEventListener.receive(decodeEvent); + } + } + + public void processEndChannelGrant() + { + //TODO: process the CSBK opcode that indicates traffic channel teardown + } + + /** + * 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) + { + int lsn = channel.getLogicalSlotNumber(); + + DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn); + + if(event != null && isSameCall(identifierCollection, event.getIdentifierCollection())) + { + 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); + + DMRChannelGrantEvent continuationGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) + .channel(channel) + .eventDescription(getEventType(opcode, event.isEncrypted()).toString() + " - Continue") + .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) + .identifiers(identifierCollection) + .build(); + + mLSNGrantEventMap.put(lsn, continuationGrantEvent); + broadcast(continuationGrantEvent); + } + } + + //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 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 && frequency != mCurrentControlFrequency && + !mAllocatedTrafficChannelFrequencyMap.containsKey(channel.getDownlinkFrequency()) && + !(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode())) + { + Channel trafficChannel = mAvailableTrafficChannelQueue.poll(); + + if(trafficChannel != null) + { + event.setEventDescription(getEventType(opcode, event.isEncrypted()).toString()); + event.setDetails("CHANNEL GRANT " + (encrypted ? " ENCRYPTED" : "")); + event.setChannelDescriptor(channel); + broadcast(event); + + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency(frequency); + trafficChannel.setSourceConfiguration(sourceConfig); + mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, identifierCollection)); + } + else + { + String description = (event.getEventDescription() != null ? event.getEventDescription() : "") + + " - MAX TRAFFIC CHANNELS EXCEEDED"; + event.setEventDescription(description); + } + } + + return; + } + + if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode()) + { + DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) + .encrypted(encrypted) + .channel(channel) + .eventDescription(getEventType(opcode, encrypted).toString() + " - Ignored") + .details("DATA CALL IGNORED") + .identifiers(identifierCollection) + .build(); + + mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent); + broadcast(channelGrantEvent); + return; + } + + DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) + .encrypted(encrypted) + .channel(channel) + .eventDescription(getEventType(opcode, encrypted).toString()) + .details("CHANNEL GRANT") + .identifiers(identifierCollection) + .build(); + + mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent); + + //Allocate a traffic channel for the downlink frequency if one isn't already allocated + //NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future + long frequency = channel.getDownlinkFrequency(); + + if(frequency > 0 && frequency != mCurrentControlFrequency && + !mAllocatedTrafficChannelFrequencyMap.containsKey(frequency)) + { + Channel trafficChannel = mAvailableTrafficChannelQueue.poll(); + + if(trafficChannel == null) + { + String details = (channelGrantEvent.getDetails() != null ? channelGrantEvent.getDetails() : "") + + " " + MAX_TRAFFIC_CHANNELS_EXCEEDED; + channelGrantEvent.setDetails(details); + channelGrantEvent.setEventDescription(channelGrantEvent.getEventDescription() + " - Ignored"); + } + else + { + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency(frequency); + trafficChannel.setSourceConfiguration(sourceConfig); + mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel); + broadcast(ChannelEvent.requestEnable(trafficChannel)); + } + } + + broadcast(channelGrantEvent); + } + + + /** + * Creates a call event type description for the specified opcode and service options + */ + private DecodeEventType getEventType(Opcode opcode, boolean encrypted) + { + DecodeEventType type = null; + + switch(opcode) + { + case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: + case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: + case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: + type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; + break; + + case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: + case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: + type = encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; + break; + + case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM: + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM: + case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT: + case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_MULTI_ITEM: + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM: + case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT: + type = encrypted ? DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL; + break; + } + + if(type == null) + { + mLog.debug("Unrecognized opcode for determining decode event type: " + opcode.name()); + type = DecodeEventType.CALL; + } + + return type; + } + + /** + * Channel event listener to receive notifications that a traffic channel has ended processing and we + * can reclaim the traffic channel for reuse. + * + * @return listener for processing channel events. + */ + @Override + public Listener getChannelEventListener() + { + return mTrafficChannelTeardownMonitor; + } + + /** + * Broadcasts a channel event to a registered external listener (for action). + */ + private void broadcast(ChannelEvent channelEvent) + { + if(mChannelEventListener != null) + { + mChannelEventListener.receive(channelEvent); + } + } + + /** + * Sets the external channel event listener to receive channel events from this traffic channel manager + */ + @Override + public void setChannelEventListener(Listener listener) + { + mChannelEventListener = listener; + } + + /** + * Removes the channel event listener + */ + @Override + public void removeChannelEventListener() + { + mChannelEventListener = null; + } + + /** + * Compares the TO role identifier(s) from each collection for equality + * + * @param collection1 containing a TO identifier + * @param collection2 containing a TO identifier + * @return true if both collections contain a TO identifier and the TO identifiers are the same value + */ + private boolean isSameCall(IdentifierCollection collection1, IdentifierCollection collection2) + { + Identifier toIdentifier1 = getIdentifier(collection1, Role.TO); + Identifier toIdentifier2 = getIdentifier(collection2, Role.TO); + return Objects.equals(toIdentifier1, toIdentifier2); + } + + /** + * Retrieves the first identifier with a TO role. + * + * @param collection containing a TO identifier + * @return TO identifier or null + */ + private Identifier getIdentifier(IdentifierCollection collection, Role role) + { + List identifiers = collection.getIdentifiers(role); + + if(identifiers.size() >= 1) + { + return identifiers.get(0); + } + + return null; + } + + /** + * Implements the IDecodeEventProvider interface to provide channel events to an external listener. + */ + @Override + public void addDecodeEventListener(Listener listener) + { + mDecodeEventListener = listener; + } + + /** + * Removes the external decode event listener + */ + @Override + public void removeDecodeEventListener(Listener listener) + { + mDecodeEventListener = null; + } + + @Override + public void reset() + { + } + + /** + * Starts this traffic channel manager. + * + * Note: for Capacity+ systems, this traffic channel manager will be reused when the current channel is in use and + * a new rest channel is nominated. This traffic channel manager instance will be transferred to the new standard + * channel created to monitor the new rest channel. As such, this manager will have a list of currently allocated + * traffic channels. Broadcast frequency lock requests for each allocated traffic channel frequency so that the + * new rest channel rotation manager doesn't rotate onto frequencies already being monitored as traffic channels. + */ + @Override + public void start() + { + for(Long frequency: mAllocatedTrafficChannelFrequencyMap.keySet()) + { + getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(frequency)); + } + } + + @Override + public void stop() + { + mAvailableTrafficChannelQueue.clear(); + + List channels = new ArrayList<>(mAllocatedTrafficChannelFrequencyMap.values()); + + //Issue a disable request for each traffic channel + for(Channel channel: channels) + { + broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); + } + } + + /** + * Monitors channel teardown events to detect when traffic channel processing has ended. Reclaims the + * channel instance for reuse by future traffic channel grants. + */ + public class TrafficChannelTeardownMonitor implements Listener + { + /** + * Removes any call events that are associated with the specified frequency. + * @param frequency that was removed + * @param description to optionally append to the details for the event + */ + private void cleanupCallEvents(long frequency, String description) + { + List lsnsToRemove = new ArrayList<>(); + + for(Map.Entry entry: mLSNGrantEventMap.entrySet()) + { + if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency) + { + lsnsToRemove.add(entry.getKey()); + } + } + + for(Integer lsn: lsnsToRemove) + { + DMRChannelGrantEvent event = mLSNGrantEventMap.remove(lsn); + + if(event != null) + { + if(description != null) + { + if(event.getEventDescription() != null) + { + event.setEventDescription(event.getEventDescription() + " - " + description); + } + else + { + event.setEventDescription(description); + } + } + + broadcast(event); + } + } + } + + @Override + public synchronized void receive(ChannelEvent channelEvent) + { + Channel channel = channelEvent.getChannel(); + + if(channel.isTrafficChannel() && (channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_STOP || + channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_START_REJECTED)) + { + String optionalDescription = (channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_START_REJECTED ? + "Rejected" : null); + + long frequencyToRemove = 0; + + for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + { + if(channel == entry.getValue() && entry.getKey() != null) + { + frequencyToRemove = entry.getKey(); + cleanupCallEvents(entry.getKey(), optionalDescription); + break; + } + } + + if(frequencyToRemove > 0) + { + 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); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java new file mode 100644 index 00000000..40aaa337 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java @@ -0,0 +1,151 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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.source.tuner.channel.ChannelSpecification; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Decoder Configuration + */ +public class DecodeConfigDMR extends DecodeConfiguration +{ + private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; + private boolean mIgnoreDataCalls = true; + private List mTimeslotMap = new ArrayList<>(); + + public DecodeConfigDMR() + { + } + + /** + * Overrides the default value to indicate that DMR has two timeslots + */ + @Override + public int getTimeslotCount() + { + return 2; + } + + /** + * Timeslot identifiers + */ + @Override + public int[] getTimeslots() + { + int[] timeslots = new int[2]; + timeslots[0] = 1; + timeslots[1] = 2; + return timeslots; + } + + @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance") + public DecoderType getDecoderType() + { + return DecoderType.DMR; + } + + /** + * Indicates if traffic channel grants for data calls should be ignored + */ + @JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls") + public boolean getIgnoreDataCalls() + { + return mIgnoreDataCalls; + } + + /** + * Sets flag to ignore data call traffic channel grants + * @param ignore true to ignore data calls. + */ + public void setIgnoreDataCalls(boolean ignore) + { + mIgnoreDataCalls = ignore; + } + + /** + * Traffic channel pool size. + * @return + */ + @JacksonXmlProperty(isAttribute = true, localName = "traffic_channel_pool_size") + public int getTrafficChannelPoolSize() + { + return mTrafficChannelPoolSize; + } + + /** + * Sets the traffic channel pool size which is the maximum number of + * simultaneous traffic channels that can be allocated. + * + * This limits the maximum calls so that busy systems won't cause more + * traffic channels to be allocated than the decoder/software/host computer + * can support. + */ + public void setTrafficChannelPoolSize(int size) + { + mTrafficChannelPoolSize = size; + } + + /** + * Source channel specification for this decoder + */ + @JsonIgnore + @Override + public ChannelSpecification getChannelSpecification() + { + return new ChannelSpecification(50000.0, 12500, 6500.0, 7200.0); + } + + /** + * Timeslot map for this decode configuration. + * + * Note: this is only used for MotoTRBO systems + */ + @JacksonXmlProperty(isAttribute = false, localName = "timeslot") + public List getTimeslotMap() + { + return mTimeslotMap; + } + + /** + * Sets the timeslot map + * @param timeslots for a MotoTRBO system + */ + public void setTimeslotMap(List timeslots) + { + mTimeslotMap = timeslots; + } + + /** + * Adds a timeslot frequency to the map + */ + @JsonIgnore + public void addTimeslotFrequency(TimeslotFrequency timeslotFrequency) + { + mTimeslotMap.add(timeslotFrequency); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/IDMRBurstDetectListener.java b/src/main/java/io/github/dsheirer/module/decode/dmr/IDMRBurstDetectListener.java new file mode 100644 index 00000000..c9d8d17b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/IDMRBurstDetectListener.java @@ -0,0 +1,44 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2019 Dennis Sheirer, Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr; + + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Listener interface to be notified each time a DMR burst has been detected or when sync is or continues to be lost. + */ +public interface IDMRBurstDetectListener +{ + /** + * Indicates that a DMR sync has been detected and binary message with the burst contents are ready for processing + * @param binaryMessage burst Binary Message and any bit errors detected in the message + * @param pattern SyncPattern + */ + void burstDetected(CorrectedBinaryMessage binaryMessage, DMRSyncPattern pattern, int timeslot); + + /** + * Indicates that sync has been lost on the dibit stream + * + * @param bitsProcessed since the last sync detect + * @param timeslot that was being tracked when the sync loss was detected + */ + void syncLost(int bitsProcessed, int timeslot); +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/TimeslotAlignmentTracker.java b/src/main/java/io/github/dsheirer/module/decode/dmr/TimeslotAlignmentTracker.java new file mode 100644 index 00000000..87094fdf --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/TimeslotAlignmentTracker.java @@ -0,0 +1,155 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr; + +import io.github.dsheirer.module.decode.dmr.message.CACH; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracks proper alignment of timeslots into the two bursts of a burst framer. + */ +public class TimeslotAlignmentTracker +{ + private final static Logger mLog = LoggerFactory.getLogger(TimeslotAlignmentTracker.class); + + private int mBurst1Ts1 = 0; + private int mBurst1Ts2 = 0; + private int mBurst2Ts1 = 0; + private int mBurst2Ts2 = 0; + + /** + * Indicates if the track has accumulated enough cach data to accurately declare timeslot alignment. The test + * checks that both timeslots each have different counts for timeslot 0 and 1 and that both timeslots each indicate + * a different dominant timeslot than the other. + */ + public boolean hasSufficientData() + { + return ((mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts2 > mBurst2Ts1) || + (mBurst1Ts1 < mBurst1Ts2 && mBurst2Ts2 < mBurst2Ts1) || + (mBurst1Ts1 == 0 && mBurst1Ts2 == 0 && mBurst2Ts1 != mBurst2Ts2) || + (mBurst1Ts1 != mBurst1Ts2 && mBurst2Ts1 == 0 && mBurst1Ts2 == 0)); + } + + /** + * Indicates if the timeslots are aligned by inspection of the CACH value for frame 0 and frame 1 + */ + public boolean isAligned() + { + return (mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts2 > mBurst2Ts1) || + (mBurst1Ts1 == 0 && mBurst1Ts2 == 0 && mBurst2Ts2 > mBurst2Ts1) || + (mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts1 == 0 && mBurst2Ts2 == 0); + } + + /** + * Updates the tracker with the cach fields or sync patterns from the current TDMA burst frames + * @param cach1 optional cach that is currently aligned as timeslot 1 + * @param cach2 optional cach that is currently aligned as timeslot 2 + * @param pattern1 sync pattern for the burst currently aligned as timeslot 1 + * @param pattern2 sync pattern for the burst currently aligned as timeslot 2 + */ + public void update(CACH cach1, CACH cach2, DMRSyncPattern pattern1, DMRSyncPattern pattern2) + { + if(cach1 != null && cach1.isValid()) + { + if(cach1.isTimeslot0()) + { + mBurst1Ts1++; + } + else + { + mBurst1Ts2++; + } + } + + if(cach2 != null && cach2.isValid()) + { + if(cach2.isTimeslot0()) + { + mBurst2Ts1++; + } + else + { + mBurst2Ts2++; + } + } + + if(pattern1.isDirectMode()) + { + switch((pattern1)) + { + case DIRECT_MODE_DATA_TIMESLOT_1: + case DIRECT_MODE_VOICE_TIMESLOT_1: + mBurst1Ts1++; + break; + case DIRECT_MODE_DATA_TIMESLOT_2: + case DIRECT_MODE_VOICE_TIMESLOT_2: + mBurst1Ts2++; + break; + } + } + + if(pattern2.isDirectMode()) + { + switch((pattern2)) + { + case DIRECT_MODE_DATA_TIMESLOT_1: + case DIRECT_MODE_VOICE_TIMESLOT_1: + mBurst2Ts1++; + break; + case DIRECT_MODE_DATA_TIMESLOT_2: + case DIRECT_MODE_VOICE_TIMESLOT_2: + mBurst2Ts2++; + break; + } + } + } + + /** + * Resets the tracker values. + */ + public void reset() + { + mBurst1Ts1 = 0; + mBurst1Ts2 = 0; + mBurst2Ts1 = 0; + mBurst2Ts2 = 0; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("BURST 1: ").append(mBurst1Ts1).append("/").append(mBurst1Ts2); + sb.append(" BURST 2:").append(mBurst2Ts1).append("/").append(mBurst2Ts2); + if(hasSufficientData()) + { + if(isAligned()) + { + sb.append(" **ALIGNED**"); + } + else + { + sb.append(" >>MIS-ALIGNED<<"); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java new file mode 100644 index 00000000..93b48d7d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java @@ -0,0 +1,80 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr.channel; + +/** + * DMR Channel with Absolute frequency values + */ +public class DMRAbsoluteChannel extends DMRChannel +{ + private long mDownlinkFrequency; + private long mUplinkFrequency; + + /** + * Constructs an instance + * @param lcn logical channel number + * @param timeslot for the channel + * @param downlinkFrequency frequency in hz + * @param uplinkFrequency frequency in hz + */ + public DMRAbsoluteChannel(int lcn, int timeslot, long downlinkFrequency, long uplinkFrequency) + { + super(lcn, timeslot); + mDownlinkFrequency = downlinkFrequency; + mUplinkFrequency = uplinkFrequency; + } + + /** + * Downlink frequency + * @return value in Hertz + */ + @Override + public long getDownlinkFrequency() + { + return mDownlinkFrequency; + } + + /** + * Uplink frequency + * @return value in Hertz + */ + @Override + public long getUplinkFrequency() + { + return mUplinkFrequency; + } + + /** + * Formatted channel number + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append(getValue()); + sb.append(" TS:").append(getTimeslot()); + sb.append(" ").append(getDownlinkFrequency() / 1E6d); + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java new file mode 100644 index 00000000..bdfc361d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java @@ -0,0 +1,141 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.channel; + +import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; +import io.github.dsheirer.protocol.Protocol; +import org.apache.commons.lang3.Validate; + +/** + * Base DMR Channel + * + * Note: timeslots are tracked as 1 and 2 + */ +public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor +{ + private int mTimeslot; + + /** + * Constructs an instance + * @param channel number or repeater number, one-based repeater number. + * @param timeslot in range: 1 or 2 + */ + public DMRChannel(int channel, int timeslot) + { + super(channel, IdentifierClass.NETWORK, Form.CHANNEL, Role.BROADCAST); + Validate.inclusiveBetween(0, 2, timeslot, "Timeslot must be between 1 and 2"); + mTimeslot = timeslot; + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + /** + * Repeater number for the channel. + */ + public int getRepeater() + { + return getValue(); + } + + /** + * Timeslot for the channel. + * @return timeslot as zero-based index with values in range: 0 or 1. + */ + public int getTimeslot() + { + return mTimeslot; + } + + /** + * Logical slot number for this channel. LSN is a 1-based index value where repeater one, timeslot 1 is + * LSN 1, timeslot 2 is LSN 2, etc. + * + * Formula: LSN = ((channel - 1) * 2) + timeslot + * + * @return logical slot number, a 1-based index value + */ + public int getLogicalSlotNumber() + { + int repeater = getRepeater(); + + if(repeater > 0) + { + return ((repeater - 1) * 2) + getTimeslot(); + } + + return 0; + } + + /** + * Returns an array of length 1 containing this channel's logical slot number + */ + public int[] getLSNArray() + { + int[] logicalSlotNumbers = new int[1]; + logicalSlotNumbers[0] = getLogicalSlotNumber(); + return logicalSlotNumbers; + } + + /** + * Number of timeslots for the DMR channel. + * @return 2 always. + */ + @Override + public int getTimeslotCount() + { + return 2; + } + + /** + * Indicates (true) that this is a DMR TDMA channel. + */ + @Override + public boolean isTDMAChannel() + { + return true; + } + + /** + * Not implemented + */ + @Override + public int[] getFrequencyBandIdentifiers() + { + return new int[0]; + } + + /** + * Not implemented. + */ + @Override + public void setFrequencyBand(IFrequencyBand bandIdentifier) + { + throw new IllegalArgumentException("This method is not supported"); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java new file mode 100644 index 00000000..79fd764a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java @@ -0,0 +1,94 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr.channel; + +/** + * DMR logical channel. This channel uses a logical channel number and a timeslot. + */ +public class DMRLogicalChannel extends DMRChannel +{ + private TimeslotFrequency mTimeslotFrequency; + + /** + * Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the + * calculated logical slot value for visual compatibility for users. + * + * @param channel number or repeater number + * @param logicalSlotNumber - zero based index. + */ + public DMRLogicalChannel(int channel, int timeslot) + { + super(channel, timeslot); + } + + /** + * Downlink frequency + * @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping + */ + @Override + public long getDownlinkFrequency() + { + if(mTimeslotFrequency != null) + { + return mTimeslotFrequency.getDownlinkFrequency(); + } + + return 0; + } + + /** + * Uplink frequency + * @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping + */ + @Override + public long getUplinkFrequency() + { + if(mTimeslotFrequency != null) + { + return mTimeslotFrequency.getUplinkFrequency(); + } + + return 0; + } + + /** + * Sets the timeslot frequency mapping + * @param timeslotFrequency + */ + public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency) + { + mTimeslotFrequency = timeslotFrequency; + } + + /** + * Formatted channel number + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("LSN:").append(getLogicalSlotNumber()); + sb.append(" LCN:").append(getValue()); + sb.append(" TS:").append(getTimeslot()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/ITimeslotFrequencyReceiver.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/ITimeslotFrequencyReceiver.java new file mode 100644 index 00000000..a1f5b245 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/ITimeslotFrequencyReceiver.java @@ -0,0 +1,36 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.channel; + +import java.util.List; + +public interface ITimeslotFrequencyReceiver +{ + /** + * Provides the logical slot number(s) that require a matching timeslot frequency mapping + */ + public int[] getLogicalTimeslotNumbers(); + + /** + * Applies the list of timeslot frequency mappings to the implementer + * @param timeslotFrequencies that match the logical timeslots + */ + public void apply(List timeslotFrequencies); +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java new file mode 100644 index 00000000..08adf174 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java @@ -0,0 +1,174 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.channel; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import javafx.beans.Observable; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleLongProperty; +import javafx.util.Callback; + +/** + * Maps a timeslot number to a pair of channel frequency values + */ +public class TimeslotFrequency +{ + private IntegerProperty mNumberProperty = new SimpleIntegerProperty(); + private LongProperty mDownlinkFrequencyProperty = new SimpleLongProperty(); + private LongProperty mUplinkFrequencyProperty = new SimpleLongProperty(); + private DoubleProperty mDownlinkMhzProperty = new SimpleDoubleProperty(); + private DoubleProperty mUplinkMhzProperty = new SimpleDoubleProperty(); + + /** + * Constructs an instance + */ + public TimeslotFrequency() + { + } + + /** + * Creates a deep copy of this instance + */ + public TimeslotFrequency copy() + { + TimeslotFrequency copy = new TimeslotFrequency(); + copy.setNumber(getNumber()); + copy.setDownlinkFrequency(getDownlinkFrequency()); + copy.setUplinkFrequency(getUplinkFrequency()); + return copy; + } + + /** + * Logical Slot Number property + */ + @JsonIgnore + public IntegerProperty getNumberProperty() + { + return mNumberProperty; + } + + /** + * Downlink Frequency property + */ + @JsonIgnore + public LongProperty downlinkFrequencyPropertyProperty() + { + return mDownlinkFrequencyProperty; + } + + /** + * Uplink Frequency property + */ + @JsonIgnore + public LongProperty uplinkFrequencyProperty() + { + return mUplinkFrequencyProperty; + } + + @JsonIgnore + public DoubleProperty getDownlinkMHz() + { + return mDownlinkMhzProperty; + } + + @JsonIgnore + public DoubleProperty getUplinkMHz() + { + return mUplinkMhzProperty; + } + + /** + * Logical slot number (LSN) as a 1-index based counter + */ + @JacksonXmlProperty(isAttribute = true, localName = "lsn") + public int getNumber() + { + return mNumberProperty.get(); + } + + /** + * Sets the logical slot number (LSN) as a 1-index start + * @param lsn where LSN 1 is the first slot, 2 the second, etc + */ + public void setNumber(int lsn) + { + mNumberProperty.set(lsn); + } + + /** + * Downlink frequency + * @return value in hertz + */ + @JacksonXmlProperty(isAttribute = true, localName = "downlink") + public long getDownlinkFrequency() + { + return mDownlinkFrequencyProperty.get(); + } + + /** + * Sets the downlink frequency value + * @param downlinkFrequency in hertz + */ + public void setDownlinkFrequency(long downlinkFrequency) + { + mDownlinkFrequencyProperty.set(downlinkFrequency); + getDownlinkMHz().set(downlinkFrequency / 1E6); + } + + /** + * Uplink frequency + * @return value in hertz + */ + @JacksonXmlProperty(isAttribute = true, localName = "uplink") + public long getUplinkFrequency() + { + return mUplinkFrequencyProperty.get(); + } + + /** + * Sets the uplink frequency value + * @param uplinkFrequency in hertz + */ + public void setUplinkFrequency(long uplinkFrequency) + { + mUplinkFrequencyProperty.set(uplinkFrequency); + getUplinkMHz().set(uplinkFrequency / 1E6); + } + + @Override + public String toString() + { + return "TIMESLOT LSN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency(); + } + + /** + * Creates an observable property extractor for use with observable lists to detect changes internal to this object. + */ + public static Callback extractor() + { + return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyPropertyProperty(), + tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()}; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java b/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java new file mode 100644 index 00000000..5d1d1e77 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java @@ -0,0 +1,178 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.event; + +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; + +/** + * 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 timestamp for the event + */ + public DMRChannelGrantEvent(long timestamp) + { + super(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(long timeStart) + { + return new DMRChannelGrantDecodeEventBuilder(timeStart); + } + + /** + * Builder pattern for constructing decode events. + */ + public static class DMRChannelGrantDecodeEventBuilder + { + protected long mTimeStart; + protected long mDuration; + protected String mEventDescription; + 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(long timeStart) + { + 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 channelDescriptor + */ + public DMRChannelGrantDecodeEventBuilder channel(DMRChannel channel) + { + mChannel = channel; + return this; + } + + /** + * Sets the event description text + * @param description of the event + */ + public DMRChannelGrantDecodeEventBuilder eventDescription(String description) + { + mEventDescription = description; + 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(mTimeStart); + decodeEvent.setChannelDescriptor(mChannel); + decodeEvent.setTimeslot(mChannel.getTimeslot()); + decodeEvent.setDetails(mDetails); + decodeEvent.setDuration(mDuration); + decodeEvent.setEventDescription(mEventDescription); + decodeEvent.setIdentifierCollection(mIdentifierCollection); + return decodeEvent; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRDecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRDecodeEvent.java new file mode 100644 index 00000000..a69f0ab1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRDecodeEvent.java @@ -0,0 +1,52 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.event; + +import io.github.dsheirer.module.decode.event.DecodeEvent; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR Decode Event + */ +public class DMRDecodeEvent extends DecodeEvent +{ + /** + * Constucts a decode event + * @param start + */ + public DMRDecodeEvent(long start) + { + super(start); + setProtocol(Protocol.DMR); + } + + /** + * Creates a new decode event builder with the specified start timestamp. + * @param timeStart for the event + * @return builder + */ + public static DecodeEventBuilder builder(long timeStart) + { + DecodeEventBuilder decodeEventBuilder = new DecodeEventBuilder(timeStart); + decodeEventBuilder.protocol(Protocol.DMR); + return decodeEventBuilder; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRLocation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRLocation.java new file mode 100644 index 00000000..bca11328 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRLocation.java @@ -0,0 +1,60 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.location.LocationIdentifier; +import io.github.dsheirer.identifier.location.Point; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR location identifier for user radio reported GPS positions. + */ +public class DMRLocation extends LocationIdentifier +{ + /** + * Constructs an instance + * + * @param value location + * @param role to or from + */ + public DMRLocation(Point value, Role role) + { + super(value, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + /** + * Utility method to create a new DMR location + * @param latitude of the position + * @param longitude of the position + * @return constructed DMR location identifier + */ + public static DMRLocation createFrom(double latitude, double longitude) + { + Point point = new Point(latitude, longitude); + return new DMRLocation(point, Role.FROM); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRNetwork.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRNetwork.java new file mode 100644 index 00000000..07f2373b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRNetwork.java @@ -0,0 +1,52 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR Network Identifier + */ +public class DMRNetwork extends IntegerIdentifier +{ + /** + * Creates an instance + * @param value of the network + */ + public DMRNetwork(int value) + { + super(value, IdentifierClass.NETWORK, Form.NETWORK, Role.BROADCAST); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + public static DMRNetwork create(int network) + { + return new DMRNetwork(network); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRRadio.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRRadio.java new file mode 100644 index 00000000..fec0c867 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRRadio.java @@ -0,0 +1,68 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR Radio Identifier + */ +public class DMRRadio extends RadioIdentifier +{ + public DMRRadio(Integer value, Role role) + { + super(value, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + /** + * Creates a DMR TO radio identifier + */ + public static RadioIdentifier createTo(int radioId) + { + return new DMRRadio(radioId, Role.TO); + } + + /** + * Creates a DMR FROM radio identifier + */ + public static RadioIdentifier createFrom(int radioId) + { + return new DMRRadio(radioId, Role.FROM); + } + + /** + * Creates a DMR ANY radio identifier + */ + public static RadioIdentifier createAny(int radioId) + { + return new DMRRadio(radioId, Role.ANY); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRSite.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRSite.java new file mode 100644 index 00000000..07cea735 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRSite.java @@ -0,0 +1,53 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.site.SiteIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR Radio Site Identifier + */ +public class DMRSite extends SiteIdentifier +{ + /** + * Constructs an instance + * @param site id + */ + public DMRSite(int site) + { + super(site); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + /** + * Utility method to create a site identifier + */ + public static DMRSite create(int site) + { + return new DMRSite(site); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java new file mode 100644 index 00000000..dcc62263 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java @@ -0,0 +1,62 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.protocol.Protocol; + +public class DMRTalkgroup extends TalkgroupIdentifier +{ + public DMRTalkgroup(Integer value) + { + super(value, Role.TO); + } + + public DMRTalkgroup(Integer value, Role role) + { + super(value, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + /** + * Creates a DMR TO talkgroup identifier + */ + public static TalkgroupIdentifier create(int talkgroup) + { + return new DMRTalkgroup(talkgroup); + } + + /** + * Creates a DMR talkgroup identifier with ANY role + */ + public static TalkgroupIdentifier createAny(int talkgroup) + { + return new DMRTalkgroup(talkgroup, Role.ANY); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRToneIdentifier.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRToneIdentifier.java new file mode 100644 index 00000000..d3f376f0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRToneIdentifier.java @@ -0,0 +1,49 @@ +/* + * + * * ****************************************************************************** + * * 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 + * * ***************************************************************************** + * + * + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.tone.ToneIdentifier; +import io.github.dsheirer.identifier.tone.ToneSequence; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR AMBE audio codec tone metadata + */ +public class DMRToneIdentifier extends ToneIdentifier +{ + public DMRToneIdentifier(ToneSequence value) + { + super(value); + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } + + public static DMRToneIdentifier create(ToneSequence value) + { + return new DMRToneIdentifier(value); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRUnitStatus.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRUnitStatus.java new file mode 100644 index 00000000..c4cbc484 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRUnitStatus.java @@ -0,0 +1,40 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.identifier; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.status.UnitStatusIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR Unit Status + */ +public class DMRUnitStatus extends UnitStatusIdentifier +{ + public DMRUnitStatus(int value) + { + super(value, Role.FROM, Protocol.DMR); + } + + public static DMRUnitStatus create(int value) + { + return new DMRUnitStatus(value); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/CACH.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/CACH.java new file mode 100644 index 00000000..e8dbe5b2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/CACH.java @@ -0,0 +1,187 @@ +package io.github.dsheirer.module.decode.dmr.message; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.message.type.LCSS; + +/** + * Common Announcement Channel is a 24-bit sequence that precedes an outbound DMR frame transmitted by a repeater. + */ +public class CACH +{ + private static final int CACH_MESSAGE_LENGTH = 24; + private static final int[] INTERLEAVE_MATRIX = new int[]{0, 4, 8, 12, 14, 18, 22, 1, 2, 3, 5, 6, 7, 9, 10, 11, 13, + 15, 16, 17, 19, 20, 21, 23}; + private static final int INBOUND_CHANNEL_ACCESS_TYPE = 0; + private static final int OUTBOUND_BURST_TIMESLOT = 1; + private static final int[] LINK_CONTROL_START_STOP = new int[]{2, 3}; + private static final int[] CACH_CRC = new int[]{4, 5, 6}; + private static final int[] CHECKSUMS = new int[]{5, 7, 6, 3}; + private static final int PAYLOAD_START = 7; + private static final int PAYLOAD_END = 24; + + public enum AccessType {IDLE, BUSY}; + + private CorrectedBinaryMessage mMessage; + private boolean mValid; + + /** + * Constructs an instance. Note: constructor is private. Use the getCACH() method to extract a CACH from a + * raw transmitted DMR burst frame. + * + * @param message containing the deinterleaved and error corrected CACH binary message + * @param valid to indicate if the deinterleaved message passed CRC check + */ + public CACH(CorrectedBinaryMessage message, boolean valid) + { + mMessage = message; + mValid = valid; + } + + /** + * Descrambled message bits for this cach + */ + public CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Indicates if this CACH has the MSB flipped to indicate Hytera XPT cach scrambling. + */ + public boolean hasHyteraCachScrambling() + { + if(!isValid()) + { + BinaryMessage copy = getMessage().copy(); + copy.flip(0); + if(getCrcChecksum(copy) == 0) + { + return true; + } + } + + return false; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); +// sb.append(getMessage().toHexString()).append(" "); +// sb.append(getInboundChannelAccessType().name()).append(" "); + sb.append(getLCSS()); +// sb.append(" (").append(getTimeslot()).append("|").append(getCrcChecksum(getMessage())).append(")"); +// sb.append(" ").append(getPayload().toHexString()); + return sb.toString(); + } + + /** + * Indicates if this cach passes the FEC error correction and indicates that the inbound channel Access Type, + * Timeslot and LCSS information is valid. + */ + public boolean isValid() + { + return mValid; + } + + /** + * Indicates the state of the next inbound channel timeslot, IDLE or BUSY + */ + public AccessType getInboundChannelAccessType() + { + if(mMessage.get(INBOUND_CHANNEL_ACCESS_TYPE)) + { + return AccessType.BUSY; + } + else + { + return AccessType.IDLE; + } + } + + /** + * Link Control or CSBK Start/Stop fragment indicator + */ + public LCSS getLCSS() + { + return LCSS.fromValue(mMessage.getInt(LINK_CONTROL_START_STOP)); + } + + /** + * Indicates the outbound timeslot for the next frame that follows this CACH, 0 or 1 + */ + public int getTimeslot() + { + return mMessage.get(OUTBOUND_BURST_TIMESLOT) ? 1 : 0; + } + + /** + * Indicates if the timeslot is 0 + */ + public boolean isTimeslot0() + { + return !mMessage.get(OUTBOUND_BURST_TIMESLOT); + } + + /** + * Binary Payload (17-bit) message fragment + */ + public BinaryMessage getPayload() + { + return mMessage.getSubMessage(PAYLOAD_START, PAYLOAD_END); + } + + /** + * Utility method to create a CACH from a raw transmitted DMR burst frame. Performs deinterleaving and error + * correction. + * + * @return constructed CACH message. + */ + public static CACH getCACH(CorrectedBinaryMessage message) + { + CorrectedBinaryMessage decodedMessage = new CorrectedBinaryMessage(CACH_MESSAGE_LENGTH); + + + //Deinterleave the transmitted message to create the decoded message + for(int x = 0; x < CACH_MESSAGE_LENGTH; x++) + { + if(message.get(INTERLEAVE_MATRIX[x])) + { + decodedMessage.set(x); + } + } + + int checksum = getCrcChecksum(decodedMessage); + +// //debug - investigating corruption of CACH, SLC and EMB during encrypted Hytera/MotoTRBO calls. +// BinaryMessage test = message.getSubMessage(0, CACH_MESSAGE_LENGTH); +// System.out.println("--> CACH:" + test.toString() + " DE:" + decodedMessage.toString() + " CHK:" + checksum); + + boolean valid = (checksum == 0); + + return new CACH(decodedMessage, valid); + } + + /** + * Calculates the CRC checksum residual. Calculates what the checkcum should be and XORs this value with the + * transmitted checksum and returns the residual value. A residual of 0 indicates a valid CRC check. + * @param message to check + * @return checsum residual value + */ + public static int getCrcChecksum(BinaryMessage message) + { + //Perform error detection + int checksum = message.getInt(CACH_CRC); + + for(int x = 0; x < 4; x++) + { + if(message.get(x)) + { + checksum ^= CHECKSUMS[x]; + } + } + + return checksum; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRBurst.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRBurst.java new file mode 100644 index 00000000..5a8c8f90 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRBurst.java @@ -0,0 +1,56 @@ +package io.github.dsheirer.module.decode.dmr.message; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +/** + * Base DMR Burst Message class + */ +public abstract class DMRBurst extends DMRMessage +{ + public static final int PAYLOAD_1_START = 24; + public static final int SYNC_START = 132; + public static final int PAYLOAD_2_START = 180; + + private DMRSyncPattern mSyncPattern; + private CACH mCACH; + + /** + * DMR message frame. This message is comprised of a 24-bit prefix and a 264-bit message frame. Outbound base + * station frames transmit a Common Announcement Channel (CACH) in the 24-bit prefix, whereas Mobile inbound frames + * do not use the 24-bit prefix. + * + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + * @param timestamp of the message + */ + public DMRBurst(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + mSyncPattern = syncPattern; + mCACH = cach; + } + + /** + * Common Announcement Channel (CACH) message frame. Note: check hasCACH() before accessing this method. + * @return CACH frame or null if this message does not contain a CACH. + */ + public CACH getCACH() + { + return mCACH; + } + + /** + * Indicates if this frame contains Common Announcement Channel (CACH) frame data. + */ + public boolean hasCACH() + { + return getSyncPattern().hasCACH(); + } + + /** + * DMR Sync pattern used by this message + */ + public DMRSyncPattern getSyncPattern() + { + return mSyncPattern; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessage.java new file mode 100644 index 00000000..e7a897b0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessage.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.protocol.Protocol; + +/** + * Base DMR Message + */ +public abstract class DMRMessage extends Message +{ + private CorrectedBinaryMessage mCorrectedBinaryMessage; + private boolean mValid = true; + private int mTimeslot; + + /** + * Constructs an instance + * @param timestamp for the message + */ + public DMRMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(timestamp); + mCorrectedBinaryMessage = message; + mTimeslot = timeslot; + } + + /** + * Message bits + */ + public CorrectedBinaryMessage getMessage() + { + return mCorrectedBinaryMessage; + } + + /** + * Indicates if this message is valid + */ + public boolean isValid() + { + return mValid; + } + + /** + * Sets the valid flag for this message + */ + public void setValid(boolean valid) + { + mValid = valid; + } + + /** + * Timeslot for this message + * @return 0 or 1 + */ + public int getTimeslot() + { + return mTimeslot; + } + + @Override + public Protocol getProtocol() + { + return Protocol.DMR; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java new file mode 100644 index 00000000..cfd84c77 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java @@ -0,0 +1,86 @@ +package io.github.dsheirer.module.decode.dmr.message; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.data.DMRDataMessageFactory; +import io.github.dsheirer.module.decode.dmr.message.voice.VoiceAMessage; +import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; +import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DMRMessageFactory +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRMessageFactory.class); + + /** + * Creates a DMR message wrapper for the binary message. DMR message type is specified by the sync pattern. + * @param syncPattern for the message + * @param binaryMessage containing the raw DMR message burst + * @param timestamp for the burst + * @param timeslot for the burst + * @return DMRMessage instance or null for UNKNOWN sync pattern + */ + public static DMRBurst create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach, + long timestamp, int timeslot) + { + switch(syncPattern) + { + case BASE_STATION_VOICE: + case MOBILE_STATION_VOICE: + case BS_VOICE_FRAME_B: + case BS_VOICE_FRAME_C: + case BS_VOICE_FRAME_D: + case BS_VOICE_FRAME_E: + case BS_VOICE_FRAME_F: + return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, timeslot); + case DIRECT_MODE_VOICE_TIMESLOT_1: + return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 1); + case DIRECT_MODE_VOICE_TIMESLOT_2: + return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 2); + case BASE_STATION_DATA: + case MOBILE_STATION_DATA: + return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot); + case DIRECT_MODE_DATA_TIMESLOT_1: + return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1); + case DIRECT_MODE_DATA_TIMESLOT_2: + return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2); + case UNKNOWN: + return null; + case MOBILE_STATION_REVERSE_CHANNEL: + case RESERVED: + default: + return new UnknownDMRMessage(syncPattern, binaryMessage, cach, timestamp, timeslot); + } + } + + /** + * Creates a voice message from the raw transmitted DMR burst frame + * @param syncPattern for the frame + * @param message containing the DMR burst + * @param cach extracted from the first 24 bits of the message + * @param timestamp the message was received + * @param timeslot for the DMR burst + * @return constructed voice message + */ + private static VoiceMessage createVoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + long timestamp, int timeslot) + { + switch(syncPattern) + { + case BASE_STATION_VOICE: + case MOBILE_STATION_VOICE: + case DIRECT_MODE_VOICE_TIMESLOT_1: + case DIRECT_MODE_VOICE_TIMESLOT_2: + return new VoiceAMessage(syncPattern, message, cach, timestamp, timeslot); + case BS_VOICE_FRAME_B: + case BS_VOICE_FRAME_C: + case BS_VOICE_FRAME_D: + case BS_VOICE_FRAME_E: + case BS_VOICE_FRAME_F: + default: + return new VoiceEMBMessage(syncPattern, message, cach, timestamp, timeslot); + } + } +} + diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/UnknownDMRMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/UnknownDMRMessage.java new file mode 100644 index 00000000..d9b7a822 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/UnknownDMRMessage.java @@ -0,0 +1,71 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; + +import java.util.Collections; +import java.util.List; + +/** + * Unknown DMR message used as a default case when the message type cannot be determined. + */ +public class UnknownDMRMessage extends DMRBurst +{ + /** + * Constructs an instance + * + * @param syncPattern + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + * @param cach + * @param timestamp + * @param timeslot + */ + public UnknownDMRMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, int timeslot) + { + super(syncPattern, message, cach, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(hasCACH()) + { + sb.append(getCACH().toString()); + } + else + { + sb.append("TS").append(getTimeslot()); + } + sb.append(" ").append(getSyncPattern()); + sb.append(" UNKNOWN DMR BURST"); + sb.append(" ").append(getMessage().toHexString()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java new file mode 100644 index 00000000..3fd87385 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java @@ -0,0 +1,262 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.BPTC_196_96; +import io.github.dsheirer.edac.CRCDMR; +import io.github.dsheirer.edac.trellis.ViterbiDecoder_3_4_DMR; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock1Rate; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock1_2Rate; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock3_4Rate; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory; +import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage; +import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ShortDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.StatusDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; +import io.github.dsheirer.module.decode.dmr.message.data.usb.USBData; +import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for creating data messages that contain a 196-bit BPTC protected message. + */ +public class DMRDataMessageFactory +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRDataMessageFactory.class); + private static final ViterbiDecoder_3_4_DMR VITERBI_DECODER = new ViterbiDecoder_3_4_DMR(); + + /** + * Creates a data message class + * @param pattern for the DMR burst + * @param message DMR burst as transmitted + * @param cach from the DMR burst + * @param timestamp for the message + * @param timeslot for the message + * @return data message instance + */ + public static DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, long timestamp, + int timeslot) + { + SlotType slotType = SlotType.getSlotType(message); + + if(slotType.isValid()) + { + switch(slotType.getDataType()) + { + case SLOT_IDLE: + return new IDLEMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case CSBK: + return CSBKMessageFactory.create(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case USB_DATA: + CorrectedBinaryMessage usbdPayload = getPayload(message); + switch(USBData.getServiceType(usbdPayload)) + { + case LIP_SHORT_LOCATION_REQUEST: + default: + return new USBData(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + } + case MBC_ENC_HEADER: + case MBC_HEADER: + return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case CHANNEL_CONTROL_ENC_HEADER: + case PI_HEADER: + return new HeaderMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case VOICE_HEADER: + return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case DATA_ENC_HEADER: + case DATA_HEADER: + CorrectedBinaryMessage payload = getPayload(message); + int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC); + boolean valid = crcBitCount < 2; + DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload); + + switch(dpf) + { + case CONFIRMED_DATA_PACKET: + ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType, timestamp, timeslot); + cdh.setValid(valid); + return cdh; + case PROPRIETARY_DATA_PACKET: + Vendor vendor = ProprietaryDataHeader.getVendor(payload); + + switch(vendor) + { + case MOTOROLA_CAPACITY_PLUS: + case MOTOROLA_CONNECT_PLUS: + ServiceAccessPoint sap = ProprietaryDataHeader.getServiceAccessPoint(payload); + + if(sap == ServiceAccessPoint.SAP_1) + { + MNISProprietaryDataHeader mpdh = new MNISProprietaryDataHeader(pattern, payload, + cach, slotType, timestamp, timeslot); + mpdh.setValid(valid); + return mpdh; + } + else + { + MotorolaProprietaryDataHeader mprdh = new MotorolaProprietaryDataHeader(pattern, + payload, cach, slotType, timestamp, timeslot); + mprdh.setValid(valid); + return mprdh; + } + case HYTERA_68: + HyteraProprietaryDataHeader hpdh = new HyteraProprietaryDataHeader(pattern, payload, + cach, slotType, timestamp, timeslot); + hpdh.setValid(valid); + return hpdh; + default: + ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach, + slotType, timestamp, timeslot); + pdh.setValid(valid); + return pdh; + } + case RAW_OR_STATUS_SHORT_DATA: + int appendedBlockCount = ShortDataHeader.getAppendedBlocks(payload); + + if(appendedBlockCount == 0) + { + StatusDataHeader sdh = new StatusDataHeader(pattern, payload, cach, slotType, timestamp, + timeslot); + sdh.setValid(valid); + return sdh; + } + else + { + RawShortDataHeader rsdh = new RawShortDataHeader(pattern, payload, cach, slotType, + timestamp, timeslot); + rsdh.setValid(valid); + return rsdh; + } + case RESPONSE_PACKET: + ResponseDataHeader rdh = new ResponseDataHeader(pattern, payload, cach, slotType, timestamp, + timeslot); + rdh.setValid(valid); + return rdh; + case DEFINED_SHORT_DATA: + DefinedShortDataHeader dsdh = new DefinedShortDataHeader(pattern, payload, cach,slotType, + timestamp, timeslot); + dsdh.setValid(valid); + return dsdh; + case UNCONFIRMED_DATA_PACKET: + UnconfirmedDataHeader udh = new UnconfirmedDataHeader(pattern, payload, cach, slotType, + timestamp, timeslot); + udh.setValid(valid); + return udh; + case UNIFIED_DATA_TRANSPORT: + //TODO: this will eventually need its own factory + UDTHeader uh = new UDTHeader(pattern, payload, cach, slotType, timestamp, timeslot); + uh.setValid(valid); + return uh; + default: + DataHeader dh = new DataHeader(pattern, payload, cach, slotType, timestamp, timeslot); + dh.setValid(valid); + return dh; + } + case TLC: + return new Terminator(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case RATE_1_OF_2_DATA: + return new DataBlock1_2Rate(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case RATE_3_OF_4_DATA: + return new DataBlock3_4Rate(pattern, getTrellisPayload(message), cach, slotType, timestamp, timeslot); + case RATE_1_DATA: + return new DataBlock1Rate(pattern, message, cach, slotType, timestamp, timeslot); + case MBC_BLOCK: + return new MBCContinuationBlock(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + case RESERVED_15: + case UNKNOWN: + return new UnknownDataMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + } + } + + return new UnknownDataMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot); + } + + /** + * Decodes a 3/4 rate trellis coded message + * @param message containing trellis coded message + * @return decoded message + */ + private static CorrectedBinaryMessage getTrellisPayload(CorrectedBinaryMessage message) + { + CorrectedBinaryMessage descrambled = extract(message); + return VITERBI_DECODER.decode(descrambled); + } + + /** + * De-scramble, decode and error check a BPTC protected raw message and return a 96-bit error corrected + * payload or null. + * @param message + * @return + */ + private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message) + { + CorrectedBinaryMessage descrambled = extract(message); + return BPTC_196_96.extract(descrambled); + } + + /** + * Extracts the 196-bit message payload from the full DMR burst + * @param message as transmitted + * @return extracted message + */ + private static CorrectedBinaryMessage extract(CorrectedBinaryMessage message) + { + CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(196); + + try + { + for(int i = 24; i < 122; i++) + { + extracted.add(message.get(i)); + } + for(int i = 190; i < 190 + 98; i++) + { + extracted.add(message.get(i)); + } + } + catch(BitSetFullException ex) + { + mLog.error("Error extracting DMR burst payload bits"); + } + + return extracted; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java new file mode 100644 index 00000000..3f19f091 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java @@ -0,0 +1,58 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.DMRBurst; + +/** + * Base DMR Data Message + */ +public abstract class DataMessage extends DMRBurst +{ + private SlotType mSlotType; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, timestamp, timeslot); + mSlotType = slotType; + } + + /** + * Slot Type identifies the color code and data type for this data message + */ + public SlotType getSlotType() + { + return mSlotType; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java new file mode 100644 index 00000000..6c9d1c41 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java @@ -0,0 +1,89 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.Collections; +import java.util.List; + +/** + * Data Message with Full Link Control Payload. + */ +public abstract class DataMessageWithLinkControl extends DataMessage +{ + private LCMessage mLCMessage; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataMessageWithLinkControl(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Access the embedded link control message + */ + public LCMessage getLCMessage() + { + if(mLCMessage == null) + { + boolean isTerminator = getSlotType().getDataType() == DataType.TLC; + mLCMessage = LCMessageFactory.createFull(getMessage(), getTimestamp(), getTimeslot(), isTerminator); + } + + return mLCMessage; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(getSlotType()); + sb.append(" ").append(getLCMessage()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + if(getLCMessage() != null) + { + return getLCMessage().getIdentifiers(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/IDLEMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/IDLEMessage.java new file mode 100644 index 00000000..4e6acbc5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/IDLEMessage.java @@ -0,0 +1,62 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; + +import java.util.Collections; +import java.util.List; + +/** + * Timeslot IDLE message + */ +public class IDLEMessage extends DataMessage +{ + /** + * Constructs an instance + * @param syncPattern for the message + * @param message payload + * @param timestamp the message was received + * @param timeslot for the message + */ + public IDLEMessage(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" IDLE"); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/SlotType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/SlotType.java new file mode 100644 index 00000000..8a7bd80d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/SlotType.java @@ -0,0 +1,122 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2020 Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.Golay24; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +public class SlotType +{ + private static final int[] MESSAGE_INDEXES = new int[]{122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 180, 181, + 182, 183, 184, 185, 186, 187, 188, 189}; + + //Note: extracted message bit index values are offset by +4 from the ICD specified index values + private static final int[] COLOR_CODE = new int[]{4, 5, 6, 7}; + private static final int[] DATA_TYPE = new int[]{8, 9, 10, 11}; + + private CorrectedBinaryMessage mMessage; + private boolean mValid; + + public SlotType(CorrectedBinaryMessage message) + { + mMessage = message; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("CC:").append(getColorCode()); + sb.append(" ").append(getDataType()); + return sb.toString(); + } + + /** + * Binary message bits + */ + private CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Sets the valid flag for this slot type. + * + * @param valid + */ + void setValid(boolean valid) + { + mValid = valid; + } + + /** + * Indicates if this message was successfully error detected and corrected + */ + public boolean isValid() + { + return mValid; + } + + /** + * SlotType message extracted from the transmitted message and Golay(24) error detection and correction complete. + * + * Note: this message is extracted with 4 leading/padding bits plus the 20 message bits for an overall length of + * 24 bits to support use of the existing Golay24 error correction utility. All bit index values are incremented + * by an offset of 4 to account for this frame offset. + */ + public static SlotType getSlotType(BinaryMessage message) + { + CorrectedBinaryMessage decodedMessage = new CorrectedBinaryMessage(24); + + for(int x = 0; x < MESSAGE_INDEXES.length; x++) + { + if(message.get(MESSAGE_INDEXES[x])) + { + decodedMessage.set(x + 4); + } + } + + int errorCount = Golay24.checkAndCorrect(decodedMessage, 0); + decodedMessage.setCorrectedBitCount(errorCount); + SlotType slotType = new SlotType(decodedMessage); + slotType.setValid(errorCount < 3); + return slotType; + } + + /** + * Color code for this timeslot. + * + * @return color code: 0 - 15 + */ + public int getColorCode() + { + return getMessage().getInt(COLOR_CODE); + } + + /** + * Data Type for this data message + */ + public DataType getDataType() + { + return DataType.fromValue(getMessage().getInt(DATA_TYPE)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/UnknownDataMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/UnknownDataMessage.java new file mode 100644 index 00000000..a31199a7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/UnknownDataMessage.java @@ -0,0 +1,75 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; + +import java.util.Collections; +import java.util.List; + +/** + * Unknown data message + */ +public class UnknownDataMessage extends DataMessage +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach + * @param slotType + * @param timestamp + * @param timeslot + */ + public UnknownDataMessage(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(hasCACH()) + { + sb.append(getCACH()); + } + else + { + sb.append("TS").append(getTimeslot()); + } + + sb.append(" ").append(getSlotType()); + sb.append(" ").append(getMessage().toHexString()); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock.java new file mode 100644 index 00000000..636025be --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.block; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessage; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.Collections; +import java.util.List; + +/** + * Base class for data blocks using 1/2, 3/4, and 1/1 trellis coded data payloads + */ +public abstract class DataBlock extends DataMessage +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataBlock(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Data block payload for confirmed delivery. + */ + public abstract CorrectedBinaryMessage getConfirmedPayload(); + + /** + * Data block payload for unconfirmed delivery + */ + public abstract CorrectedBinaryMessage getUnConfirmedPayload(); + + /** + * Serial number for a confirmed data block + */ + public abstract int getDataBlockSerialNumber(); + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append(" ").append(getSlotType()); + sb.append(" ").append(getMessage().toHexString()); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1Rate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1Rate.java new file mode 100644 index 00000000..d434d637 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1Rate.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.block; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Rate 1/1 Data Block + */ +public class DataBlock1Rate extends DataBlock +{ + private static final int[] DATA_BLOCK_SERIAL_NUMBER = new int[]{0, 1, 2, 3, 4, 5, 6}; + private static final int[] CRC = new int[]{7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int CONFIRMED_PAYLOAD_START = 16; + private static final int UNCONFIRMED_PAYLOAD_START = 0; + private static final int PAYLOAD_END = 192; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataBlock1Rate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + //TODO: check CRC for data block serial number + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("RATE 1/1 DATA CONFIRMED:").append(getConfirmedPayload().toHexString()); + sb.append(" UNCONFIRMED:").append(getUnConfirmedPayload().toHexString()); + return sb.toString(); + } + + @Override + public int getDataBlockSerialNumber() + { + return getMessage().getInt(DATA_BLOCK_SERIAL_NUMBER); + } + + @Override + public CorrectedBinaryMessage getConfirmedPayload() + { + return getMessage().getSubMessage(CONFIRMED_PAYLOAD_START, PAYLOAD_END); + } + + @Override + public CorrectedBinaryMessage getUnConfirmedPayload() + { + return getMessage().getSubMessage(UNCONFIRMED_PAYLOAD_START, PAYLOAD_END); + } +} + + diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1_2Rate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1_2Rate.java new file mode 100644 index 00000000..881b11de --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock1_2Rate.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.block; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Rate 1/2 BPTC 196 Data Block + */ +public class DataBlock1_2Rate extends DataBlock +{ + private static final int[] DATA_BLOCK_SERIAL_NUMBER = new int[]{0, 1, 2, 3, 4, 5, 6}; + private static final int[] CRC = new int[]{7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int CONFIRMED_PAYLOAD_START = 16; + private static final int UNCONFIRMED_PAYLOAD_START = 0; + private static final int PAYLOAD_END = 96; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataBlock1_2Rate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + //TODO: check CRC for data block serial number + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("RATE 1/2 DATA CONFIRMED:").append(getConfirmedPayload().toHexString()); + sb.append(" UNCONFIRMED:").append(getUnConfirmedPayload().toHexString()); + return sb.toString(); + } + + @Override + public int getDataBlockSerialNumber() + { + return getMessage().getInt(DATA_BLOCK_SERIAL_NUMBER); + } + + @Override + public CorrectedBinaryMessage getConfirmedPayload() + { + return getMessage().getSubMessage(CONFIRMED_PAYLOAD_START, PAYLOAD_END); + } + + @Override + public CorrectedBinaryMessage getUnConfirmedPayload() + { + return getMessage().getSubMessage(UNCONFIRMED_PAYLOAD_START, PAYLOAD_END); + } +} + + diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock3_4Rate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock3_4Rate.java new file mode 100644 index 00000000..f3e032c1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/DataBlock3_4Rate.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.block; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Rate 3/4 Trellis Coded Data Block + */ +public class DataBlock3_4Rate extends DataBlock +{ + private static final int[] DATA_BLOCK_SERIAL_NUMBER = new int[]{0, 1, 2, 3, 4, 5, 6}; + private static final int[] CRC = new int[]{7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int CONFIRMED_PAYLOAD_START = 16; + private static final int UNCONFIRMED_PAYLOAD_START = 0; + private static final int PAYLOAD_END = 144; + + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataBlock3_4Rate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + //TODO: check CRC for data block serial number + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("RATE 3/4 DATA CONFIRMED:").append(getConfirmedPayload().toHexString()); + sb.append(" UNCONFIRMED:").append(getUnConfirmedPayload().toHexString()); + return sb.toString(); + } + + @Override + public int getDataBlockSerialNumber() + { + return getMessage().getInt(DATA_BLOCK_SERIAL_NUMBER); + } + + @Override + public CorrectedBinaryMessage getConfirmedPayload() + { + return getMessage().getSubMessage(CONFIRMED_PAYLOAD_START, PAYLOAD_END); + } + + @Override + public CorrectedBinaryMessage getUnConfirmedPayload() + { + return getMessage().getSubMessage(UNCONFIRMED_PAYLOAD_START, PAYLOAD_END); + } +} + + diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/UnknownDataBlock.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/UnknownDataBlock.java new file mode 100644 index 00000000..6c956fe6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/block/UnknownDataBlock.java @@ -0,0 +1,65 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.block; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Unknown DMR data block + */ +public class UnknownDataBlock extends DataBlock +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public UnknownDataBlock(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + @Override + public CorrectedBinaryMessage getConfirmedPayload() + { + return getMessage(); + } + + @Override + public CorrectedBinaryMessage getUnConfirmedPayload() + { + return getMessage(); + } + + @Override + public int getDataBlockSerialNumber() + { + return 0; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessage.java new file mode 100644 index 00000000..7a77482f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessage.java @@ -0,0 +1,166 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data.csbk; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.CRCDMR; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessage; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Control Signalling Block (CSBK) Message + * + * ETSI 102 361-1 7.2.0 + */ +public abstract class CSBKMessage extends DataMessage +{ + protected static final int CSBK_CRC_MASK = 0xA5A5; + protected static final int MBC_HEADER_CRC_MASK = 0xAAAA; + protected static final int MBC_LAST_BLOCK_CRC_MASK = 0x0; + + private static final int LAST_BLOCK = 0; + private static final int PROTECT_FLAG = 1; + private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7}; + private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * 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 CSBKMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + int correctedBitCount = CRCDMR.correctCCITT80(getMessage(), 0, 80, CSBK_CRC_MASK); + + //Set message valid flag according to the corrected bit count for the CRC protected message + setValid(correctedBitCount < 2); + } + + /** + * Checks crc for multi-block CSBK where the continuation block is an absolute channel parameters block + * + * Note: this method assumes that there is just 1 continuation block and that block is used to carry + * the absolute channel parameters structure. If future implementations use more than 1 MBC continuation + * block, then the CRC check has to be performed across all of the continuation blocks where the CRC checksum is + * contained in the final continuation block. + */ + protected void checkMultiBlockCRC(AbsoluteChannelParameters absoluteChannelParameters) + { + int headerCorrectedBitCount = CRCDMR.correctCCITT80(getMessage(), 0, 80, MBC_HEADER_CRC_MASK); + + int continuationCorrectedBitCount = 0; + + if(absoluteChannelParameters != null) + { + continuationCorrectedBitCount = CRCDMR.correctCCITT80(absoluteChannelParameters.getMessage(), + 0, 80, MBC_LAST_BLOCK_CRC_MASK); + } + + //Set message valid flag according to the corrected bit count for the CRC protected messages + setValid(headerCorrectedBitCount < 2 && continuationCorrectedBitCount < 2); + } + + /** + * Indicates if this is the last/final block of a multi-block message fragment sequence. + */ + public boolean isLastBlock() + { + return getMessage().get(LAST_BLOCK); + } + + /** + * Indicates if this message is encrypted. + */ + public boolean isEncrypted() + { + return getMessage().get(PROTECT_FLAG); + } + + /** + * Utility method to lookup the opcode from a CSBK message + * @param message containing CSBK bits + * @return opcode + */ + public static Opcode getOpcode(BinaryMessage message) + { + return Opcode.fromValue(message.getInt(OPCODE), getVendor(message)); + } + + /** + * Opcode for this CSBK message + */ + public Opcode getOpcode() + { + return getOpcode(getMessage()); + } + + /** + * Opcode numeric value + */ + protected int getOpcodeValue() + { + return getMessage().getInt(OPCODE); + } + + /** + * Utility method to lookup the vendor from a CSBK message + * @param message containing CSBK bits + * @return vendor + */ + public static Vendor getVendor(BinaryMessage message) + { + return Vendor.fromValue(message.getInt(VENDOR)); + } + + /** + * Vendor for this message + */ + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Numerical value for the vendor + */ + protected int getVendorID() + { + return getMessage().getInt(VENDOR); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java new file mode 100644 index 00000000..f208426a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java @@ -0,0 +1,426 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.hytera.HyteraAdjacentSiteInformation; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement; +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.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; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusPreamble; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSystemStatus; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusCSBKO_16; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataRevertWindowAnnouncement; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataRevertWindowGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusNeighborReport; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusOTAAnnouncement; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusRegistrationRequest; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusRegistrationResponse; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusTalkgroupAffiliation; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusTerminateChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Clear; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.MoveTSCC; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.Acknowledge; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.AcknowledgeStatus; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.RegistrationAccepted; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.Ahoy; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.AuthenticateRegisterRadioCheck; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.CancelCall; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.ServiceRadioCheck; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.StunReviveKill; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.UnknownAhoy; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AdjacentSiteInformation; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AnnounceChannelFrequency; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AnnounceWithdrawTSCC; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.CallTimerParameters; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.LocalTime; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.MassRegistration; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.BroadcastTalkgroupVoiceChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.DuplexPrivateDataChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.DuplexPrivateVoiceChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.PrivateDataChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.PrivateVoiceChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.TalkgroupDataChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.TalkgroupVoiceChannelGrant; +import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.UnknownMultiCSBK; +import io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceKind; + +import java.util.List; + +/** + * Factory for creating DMR CSBK and Multi-Block CSBK messages + */ +public class CSBKMessageFactory +{ + /** + * Creates a CSBK Message + * @param pattern that was transmitted + * @param message contents + * @param cach instance + * @param slotType instance + * @param timestamp of the message + * @param timeslot the message was transmitted on + * @return constructed CSBK mesage + */ + public static CSBKMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + CSBKMessage csbk = null; + + if(message != null) + { + Opcode opcode = CSBKMessage.getOpcode(message); + + switch(opcode) + { + case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_TSCC: + case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_TSCC: + case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_PAYLOAD: + case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_PAYLOAD: + switch(Acknowledge.getReason(message)) + { + case TS_REGISTRATION_ACCEPTED: + csbk = new RegistrationAccepted(pattern, message, cach, slotType, timestamp, timeslot); + break; + case TS_ACCEPTED_FOR_STATUS_POLLING_SERVICE: + csbk = new AcknowledgeStatus(pattern, message, cach, slotType, timestamp, timeslot); + break; + default: + csbk = new Acknowledge(pattern, message, cach, slotType, timestamp, timeslot); + break; + } + break; + case STANDARD_AHOY: + ServiceKind serviceKind = Ahoy.getServiceKind(message); + switch(serviceKind) + { + case AUTHENTICATE_REGISTER_RADIO_CHECK_SERVICE: + csbk = new AuthenticateRegisterRadioCheck(pattern, message, cach, slotType, timestamp, timeslot); + break; + case CANCEL_CALL_SERVICE: + csbk = new CancelCall(pattern, message, cach, slotType, timestamp, timeslot); + break; + case SUPPLEMENTARY_SERVICE: + csbk = new StunReviveKill(pattern, message, cach, slotType, timestamp, timeslot); + break; + case FULL_DUPLEX_MS_TO_MS_PACKET_CALL_SERVICE: + case FULL_DUPLEX_MS_TO_MS_VOICE_CALL_SERVICE: + case INDIVIDUAL_VOICE_CALL_SERVICE: + case INDIVIDUAL_PACKET_CALL_SERVICE: + case INDIVIDUAL_UDT_SHORT_DATA_CALL_SERVICE: + case TALKGROUP_PACKET_CALL_SERVICE: + case TALKGROUP_UDT_SHORT_DATA_CALL_SERVICE: + case TALKGROUP_VOICE_CALL_SERVICE: + csbk = new ServiceRadioCheck(pattern, message, cach, slotType, timestamp, timeslot); + break; + default: + csbk = new UnknownAhoy(pattern, message, cach, slotType, timestamp, timeslot); + break; + } + break; + case STANDARD_ALOHA: + csbk = new Aloha(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_ANNOUNCEMENT: + switch(Announcement.getAnnouncementType(message)) + { + case ADJACENT_SITE_INFORMATION: + csbk = new AdjacentSiteInformation(pattern, message, cach, slotType, timestamp, timeslot); + break; + case ANNOUNCE_OR_WITHDRAW_TSCC: + csbk = new AnnounceWithdrawTSCC(pattern, message, cach, slotType, timestamp, timeslot); + break; + case CALL_TIMER_PARAMETERS: + csbk = new CallTimerParameters(pattern, message, cach, slotType, timestamp, timeslot); + break; + case LOCAL_TIME: + csbk = new LocalTime(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MASS_REGISTRATION: + csbk = new MassRegistration(pattern, message, cach, slotType, timestamp, timeslot); + break; + case VOTE_NOW_ADVICE: + csbk = new VoteNowAdvice(pattern, message, cach, slotType, timestamp, timeslot); + break; + default: + csbk = new Announcement(pattern, message, cach, slotType, timestamp, timeslot); + break; + } + break; + case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: + csbk = new BroadcastTalkgroupVoiceChannelGrant(pattern, message, cach, slotType, timestamp, + timeslot); + break; + case STANDARD_CLEAR: + csbk = new Clear(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT: + csbk = new DuplexPrivateDataChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: + csbk = new DuplexPrivateVoiceChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM: + csbk = new PrivateDataChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: + csbk = new PrivateVoiceChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM: + csbk = new TalkgroupDataChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: + csbk = new TalkgroupVoiceChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_MOVE_TSCC: + csbk = new MoveTSCC(pattern, message, cach, slotType, timestamp, timeslot); + break; + case STANDARD_PREAMBLE: + csbk = new Preamble(pattern, message, cach, slotType, timestamp, timeslot); + break; + + case HYTERA_08_ANNOUNCEMENT: + case HYTERA_68_ANNOUNCEMENT: + AnnouncementType announcementType = HyteraAnnouncement.getAnnouncementType(message); + switch(announcementType) + { + case ADJACENT_SITE_INFORMATION: + csbk = new HyteraAdjacentSiteInformation(pattern, message, cach, slotType, timestamp, timeslot); + break; + default: + csbk = new HyteraAnnouncement(pattern, message, cach, slotType, timestamp, timeslot); + break; + } + break; + case HYTERA_XPT_PREAMBLE: + csbk = new HyteraXPTPreamble(pattern, message, cach, slotType, timestamp, timeslot); + break; + case HYTERA_XPT_SITE_STATE: + csbk = new HyteraXPTSiteState(pattern, message, cach, slotType, timestamp, timeslot); + break; + + case MOTOROLA_CAPMAX_ALOHA: + csbk = new CapacityMaxAloha(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT: + csbk = new CapacityPlusNeighbors(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_PREAMBLE: + csbk = new CapacityPlusPreamble(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_SYSTEM_STATUS: + csbk = new CapacityPlusSystemStatus(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_DATA_WINDOW_ANNOUNCEMENT: + csbk = new CapacityPlusDataRevertWindowAnnouncement(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_DATA_WINDOW_GRANT: + csbk = new CapacityPlusDataRevertWindowGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + + case MOTOROLA_CONPLUS_CSBKO_10: + csbk = new ConnectPlusOTAAnnouncement(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_CSBKO_16: + csbk = new ConnectPlusCSBKO_16(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_REGISTRATION_REQUEST: + csbk = new ConnectPlusRegistrationRequest(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE: + csbk = new ConnectPlusRegistrationResponse(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION: + csbk = new ConnectPlusTalkgroupAffiliation(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPPLUS_CSBKO_60: + csbk = new CapacityPlusCSBKO_60(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT: + csbk = new ConnectPlusDataChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_NEIGHBOR_REPORT: + csbk = new ConnectPlusNeighborReport(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_TERMINATE_CHANNEL_GRANT: + csbk = new ConnectPlusTerminateChannelGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: + csbk = new ConnectPlusVoiceChannelUser(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_DATA_WINDOW_ANNOUNCEMENT: + csbk = new ConnectPlusDataRevertWindowAnnouncement(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CONPLUS_DATA_WINDOW_GRANT: + csbk = new ConnectPlusDataRevertWindowGrant(pattern, message, cach, slotType, timestamp, timeslot); + break; + } + + if(csbk == null) + { + csbk = new UnknownCSBKMessage(pattern, message, cach, slotType, timestamp, timeslot); + } + } + + //Check CRC and set valid flag + if(csbk != null) + { + csbk.checkCRC(); + } + + return csbk; + } + + /** + * Creates a multi-block CSBK message + * @param header for the Multi-Block CSBK (MBC) + * @param continuationBlocks that form the rest of the message + * @return message + */ + public static CSBKMessage create(MBCHeader header, List continuationBlocks) + { + CSBKMessage csbk; + + Opcode opcode = header.getOpcode(); + + switch(opcode) + { + case STANDARD_ANNOUNCEMENT: + AnnouncementType announcementType = Announcement.getAnnouncementType(header.getMessage()); + switch(announcementType) + { + case ADJACENT_SITE_INFORMATION: + csbk = new AdjacentSiteInformation(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case ANNOUNCE_OR_WITHDRAW_TSCC: + csbk = new AnnounceWithdrawTSCC(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case CHANNEL_FREQUENCY_ANNOUNCEMENT: + csbk = new AnnounceChannelFrequency(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case VOTE_NOW_ADVICE: + csbk = new VoteNowAdvice(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + default: + csbk = new UnknownMultiCSBK(header, continuationBlocks); + break; + } + break; + case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: + csbk = new BroadcastTalkgroupVoiceChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_CLEAR: + csbk = new Clear(header.getSyncPattern(), header.getMessage(), header.getCACH(), header.getSlotType(), + header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT: + csbk = new DuplexPrivateDataChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: + csbk = new DuplexPrivateVoiceChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_MOVE_TSCC: + csbk = new MoveTSCC(header.getSyncPattern(), header.getMessage(), header.getCACH(), header.getSlotType(), + header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM: + csbk = new PrivateDataChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: + csbk = new PrivateVoiceChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM: + csbk = new TalkgroupDataChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: + csbk = new TalkgroupVoiceChannelGrant(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + + case HYTERA_08_ANNOUNCEMENT: + case HYTERA_68_ANNOUNCEMENT: + AnnouncementType hyteraAnnouncementType = HyteraAnnouncement.getAnnouncementType(header.getMessage()); + switch(hyteraAnnouncementType) + { + case ADJACENT_SITE_INFORMATION: + csbk = new HyteraAdjacentSiteInformation(header.getSyncPattern(), header.getMessage(), header.getCACH(), + header.getSlotType(), header.getTimestamp(), header.getTimeslot(), getBlock1(continuationBlocks)); + break; + default: + csbk = new UnknownMultiCSBK(header, continuationBlocks); + break; + } + break; + default: + csbk = new UnknownMultiCSBK(header, continuationBlocks); + break; + } + + //Check CRC and set valid flag + if(csbk != null) + { + csbk.checkCRC(); + } + + return csbk; + } + + /** + * Extracts the first block from the list + * @param blocks containing zero or more blocks + * @return first block or null + */ + private static MBCContinuationBlock getBlock1(List blocks) + { + if(blocks != null && blocks.size() >= 1) + { + return blocks.get(0); + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java new file mode 100644 index 00000000..d1b1e9c0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java @@ -0,0 +1,203 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk; + +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.TreeMap; + +/** + * Control Signalling Block (CSBK) Opcode enumeration + * + * ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B + * ETSI TS 102 361-4 DMR Trunking Protocol, Annex B + */ +public enum Opcode +{ + STANDARD_FEATURE_NOT_SUPPORTED(Vendor.STANDARD, 3, "FEATURE NOT SUPPORTED"), + STANDARD_UNIT_TO_UNIT_VOICE_SERVICE_REQUEST(Vendor.STANDARD, 4, "UNIT TO UNIT VOICE SERVICE REQUEST"), + STANDARD_UNIT_TO_UNIT_VOICE_SERVICE_RESPONSE(Vendor.STANDARD, 5, "UNIT TO UNIT VOICE SERVICE RESPONSE"), + STANDARD_CHANNEL_TIMING(Vendor.STANDARD, 7, "CHANNEL TIMING"), + STANDARD_ALOHA(Vendor.STANDARD, 25, "ALOHA"), + STANDARD_UNIFIED_DATA_TRANSPORT_OUTBOUND_HEADER(Vendor.STANDARD, 26, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"), + STANDARD_UNIFIED_DATA_TRANSPORT_INBOUND_HEADER(Vendor.STANDARD, 27, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"), + STANDARD_AHOY(Vendor.STANDARD, 28, "AHOY"), + STANDARD_ACTIVATION(Vendor.STANDARD, 30, "ACTIVATION"), + STANDARD_RANDOM_ACCESS_SERVICE_REQUEST(Vendor.STANDARD, 31, "RANDOM ACCESS SERVICE REQUEST"), + STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_TSCC(Vendor.STANDARD, 32, "ACKNOWLEDGE RESPONSE OUTBOUND TSCC"), + STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_TSCC(Vendor.STANDARD, 33, "ACKNOWLEDGE RESPONSE INBOUND TSCC"), + STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_PAYLOAD(Vendor.STANDARD, 34, "ACKNOWLEDGE RESPONSE OUTBOUND PAYLOAD"), + STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_PAYLOAD(Vendor.STANDARD, 35, "ACKNOWLEDGE RESPONSE INBOUND PAYLOAD"), + STANDARD_UNIFIED_DATA_TRANSPORT_FOR_DGNA_OUTBOUND_HEADER(Vendor.STANDARD, 36, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"), + STANDARD_UNIFIED_DATA_TRANSPORT_FOR_DGNA_INBOUND_HEADER(Vendor.STANDARD, 37, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"), + STANDARD_ANNOUNCEMENT(Vendor.STANDARD, 40, "ANNOUNCEMENT"), + STANDARD_NEGATIVE_ACKNOWLEDGE_RESPONSE(Vendor.STANDARD, 38, "NEGATIVE ACKNOWLEDGE RESPONSE"), + STANDARD_MAINTENANCE(Vendor.STANDARD, 42, "MAINTENANCE"), + STANDARD_CLEAR(Vendor.STANDARD, 46, "CLEAR"), + STANDARD_PROTECT(Vendor.STANDARD, 47, "PROTECT"), + STANDARD_PRIVATE_VOICE_CHANNEL_GRANT(Vendor.STANDARD, 48, "PRIVATE VOICE CHANNEL GRANT"), + STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT(Vendor.STANDARD, 49, "TALKGROUP VOICE CHANNEL GRANT"), + STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT(Vendor.STANDARD, 50, "BROADCAST TALKGROUP VOICE CHANNEL GRANT"), + STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM(Vendor.STANDARD, 51, "PRIVATE DATA CHANNEL GRANT SINGLE ITEM"), + STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM(Vendor.STANDARD, 52, "TALKGROUP DATA CHANNEL GRANT SINGLE ITEM"), + STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT(Vendor.STANDARD, 53, "DUPLEX PRIVATE VOICE CHANNEL GRANT"), + STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT(Vendor.STANDARD, 54, "DUPLEX PRIVATE DATA CHANNEL GRANT"), + STANDARD_PRIVATE_DATA_CHANNEL_GRANT_MULTI_ITEM(Vendor.STANDARD, 55, "PRIVATE DATA CHANNEL GRANT MULTI ITEM"), + STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM(Vendor.STANDARD, 56, "TALKGROUP DATA CHANNEL GRANT MULTI ITEM"), + STANDARD_MOVE_TSCC(Vendor.STANDARD, 57, "MOVE TSCC"), + STANDARD_PREAMBLE(Vendor.STANDARD, 61, "PREAMBLE"), + + MOTOROLA_CONPLUS_NEIGHBOR_REPORT(Vendor.MOTOROLA_CONNECT_PLUS, 1, "NEIGHBOR REPORT"), + MOTOROLA_CONPLUS_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CONNECT_PLUS, 3, "VOICE CHANNEL USER"), + MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT(Vendor.MOTOROLA_CONNECT_PLUS, 6, "DATA CHANNEL GRANT"), + MOTOROLA_CONPLUS_CSBKO_10(Vendor.MOTOROLA_CONNECT_PLUS, 10, "CSBKO 10"), + MOTOROLA_CONPLUS_TERMINATE_CHANNEL_GRANT(Vendor.MOTOROLA_CONNECT_PLUS, 12, "TERMINATE CHANNEL GRANT"), + MOTOROLA_CONPLUS_CSBKO_16(Vendor.MOTOROLA_CONNECT_PLUS, 16, "CSBKO 16"), + MOTOROLA_CONPLUS_REGISTRATION_REQUEST(Vendor.MOTOROLA_CONNECT_PLUS, 17, "REGISTRATION REQUEST"), + MOTOROLA_CONPLUS_REGISTRATION_RESPONSE(Vendor.MOTOROLA_CONNECT_PLUS, 18, "REGISTRATION RESPONSE"), + MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION(Vendor.MOTOROLA_CONNECT_PLUS, 24, "TALKGROUP AFFILIATION"), + 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"), + + //TODO: Capacity Max (Tier III) also supports Enhanced Data Revert channels with windowing ... they either + //TODO: use CSBKO 41 and 42, or they have a different pair of messages. + MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "ALOHA"), + + MOTOROLA_CAPPLUS_CALL_ALERT(Vendor.MOTOROLA_CAPACITY_PLUS, 31, "CALL ALERT"), + MOTOROLA_CAPPLUS_CALL_ALERT_ACK(Vendor.MOTOROLA_CAPACITY_PLUS, 32, "CALL ALERT ACK"), + MOTOROLA_CAPPLUS_DATA_WINDOW_ANNOUNCEMENT(Vendor.MOTOROLA_CAPACITY_PLUS, 41, "ENHANCED DATA REVERT WINDOW ANNOUNCEMENT"), + MOTOROLA_CAPPLUS_DATA_WINDOW_GRANT(Vendor.MOTOROLA_CAPACITY_PLUS, 42, "ENHANCED DATA REVERT WINDOW GRANT"), + MOTOROLA_CAPPLUS_NEIGHBOR_REPORT(Vendor.MOTOROLA_CAPACITY_PLUS, 59, "NEIGHBOR REPORT"), + MOTOROLA_CAPPLUS_CSBKO_60(Vendor.MOTOROLA_CAPACITY_PLUS, 60, "CSBKO 60"), + MOTOROLA_CAPPLUS_PREAMBLE(Vendor.MOTOROLA_CAPACITY_PLUS, 61, "PREAMBLE"), + MOTOROLA_CAPPLUS_SYSTEM_STATUS(Vendor.MOTOROLA_CAPACITY_PLUS, 62, "SYSTEM STATUS"), + + HYTERA_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA XPT SITE STATE"), + HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "T-III 08 ANNOUNCEMENT"), + HYTERA_68_ANNOUNCEMENT(Vendor.HYTERA_68, 40, "T-III 68 ANNOUNCEMENT"), + + //Opcode 54 and/or 55 - See patent on Hytera call forwarding + HYTERA_XPT_PREAMBLE(Vendor.HYTERA_68, 61, "HYTERA XPT PREAMBLE"), + + UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN"); + + private Vendor mVendor; + private int mValue; + private String mLabel; + + Opcode(Vendor vendor, int value, String label) + { + mVendor = vendor; + mValue = value; + mLabel = label; + } + + /** + * Vendor for this opcode + */ + public Vendor getVendor() + { + return mVendor; + } + + /** + * Numeric value for the opcode + */ + public int getValue() + { + return mValue; + } + + /** + * Pretty label for the opcode + */ + public String getLabel() + { + return mLabel; + } + + @Override + public String toString() + { + return getLabel(); + } + + /** + * Indicates if this opcode is a data channel grant opcode + */ + public boolean isDataChannelGrantOpcode() + { + return DATA_CHANNEL_GRANT_OPCODES.contains(this); + } + + /** + * Data Channel Grant opcodes + */ + public static final EnumSet DATA_CHANNEL_GRANT_OPCODES = EnumSet.of( + STANDARD_PRIVATE_DATA_CHANNEL_GRANT_MULTI_ITEM, + STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM, + STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT, + STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM, + STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM); + + + /** + * Lookup map of vendors and opcode value to opcodes + */ + private static final Map> LOOKUP_MAP = new EnumMap<>(Vendor.class); + + static + { + for(Opcode opcode : Opcode.values()) + { + if(LOOKUP_MAP.containsKey(opcode.getVendor())) + { + LOOKUP_MAP.get(opcode.getVendor()).put(opcode.getValue(), opcode); + } + else + { + Map map = new TreeMap<>(); + map.put(opcode.getValue(), opcode); + LOOKUP_MAP.put(opcode.getVendor(), map); + } + } + } + + /** + * Lookup the opcode from the value for the specified vendor + */ + public static Opcode fromValue(int value, Vendor vendor) + { + if(LOOKUP_MAP.containsKey(vendor)) + { + Map opcodeMap = LOOKUP_MAP.get(vendor); + + if(opcodeMap.containsKey(value)) + { + return opcodeMap.get(value); + } + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/UnknownCSBKMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/UnknownCSBKMessage.java new file mode 100644 index 00000000..d43af5e9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/UnknownCSBKMessage.java @@ -0,0 +1,97 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.type.Vendor; + +import java.util.Collections; +import java.util.List; + +/** + * Unknown CSBK Message + */ +public class UnknownCSBKMessage extends CSBKMessage +{ + /** + * 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 UnknownCSBKMessage(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()); + sb.append(" CSBK *UNKNOWN*"); + + Vendor vendor = getVendor(); + + if(vendor == Vendor.UNKNOWN) + { + sb.append(" VENDOR:").append(getVendorID()); + } + else if(vendor != Vendor.STANDARD) + { + sb.append(" ").append(vendor); + } + + Opcode opcode = getOpcode(); + + if(opcode != Opcode.UNKNOWN) + { + sb.append(" ").append(opcode); + } + else + { + sb.append(" UNKNOWN OPCODE:").append(getOpcodeValue()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAdjacentSiteInformation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAdjacentSiteInformation.java new file mode 100644 index 00000000..10da342a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAdjacentSiteInformation.java @@ -0,0 +1,94 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.standard.announcement.AdjacentSiteInformation; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; + +/** + * Hytera Neighbor/Adjacent Site Information + */ +public class HyteraAdjacentSiteInformation extends AdjacentSiteInformation +{ + /** + * Constructs a multi-block MBC instance + * @param syncPattern for the burst + * @param message from the burst + * @param cach from the burst + * @param slotType for the burst + * @param timestamp of the burst + * @param timeslot of the burst + * @param multiBlock containing absolute frequency parameters + */ + public HyteraAdjacentSiteInformation(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + /** + * Constructs a single-block CSBK instance + * @param syncPattern for the burst + * @param message from the burst + * @param cach from the burst + * @param slotType for the burst + * @param timestamp of the burst + * @param timeslot of the burst + */ + public HyteraAdjacentSiteInformation(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()); + sb.append(" HYTERA NEIGHBOR ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getNeighborSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getNeighborSystemIdentityCode().getSite()); + if(hasAbsoluteChannelParameters()) + { + sb.append(" CHAN:").append(getAbsoluteChannelParameters().getChannel()); + sb.append(" CC:").append(getAbsoluteChannelParameters().getColorCode()); + } + else + { + sb.append(" CHAN:").append(getNeighborChannelNumber()); + } + sb.append(" THIS NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java new file mode 100644 index 00000000..fb3a9ebd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java @@ -0,0 +1,156 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera DMR Tier III - Announcement Message + */ +public class HyteraAnnouncement extends CSBKMessage +{ + private static final int[] ANNOUNCEMENT_TYPE = new int[]{16, 17, 18, 19, 20}; + private static final int PARAMS_1_START = 21; + private static final int PARAMS_1_END = 35; + private static final int[] PARAMS_1 = new int[]{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}; + private static final int REGISTRATION_REQUIRED_FLAG = 35; + private static final int[] BACKOFF = new int[]{36, 37, 38, 39}; + private static final int SYSTEM_IDENTITY_CODE_OFFSET = 40; + private static final int PARAMS_2_START = 56; + private static final int PARAMS_2_END = 80; + private static final int[] PARAMS_2 = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private SystemIdentityCode mSystemIdentityCode; + + /** + * 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 HyteraAnnouncement(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()); + sb.append(" HYTERA ").append(getAnnouncementType()); + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + sb.append(" P1:").append(getParams1().toHexString()); + sb.append(" P2:").append(getParams2().toHexString()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + public BinaryMessage getParams1() + { + return getMessage().getSubMessage(PARAMS_1_START, PARAMS_1_END); + } + + public BinaryMessage getParams2() + { + return getMessage().getSubMessage(PARAMS_2_START, PARAMS_2_END); + } + + /** + * Indicates if registration is required on this site + */ + public boolean isRegistrationRequired() + { + return getMessage().get(REGISTRATION_REQUIRED_FLAG); + } + + /** + * Backoff value + */ + public int getBackoff() + { + return getMessage().getInt(BACKOFF); + } + + /** + * System Identity Code structure + */ + public SystemIdentityCode getSystemIdentityCode() + { + if(mSystemIdentityCode == null) + { + mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, true); + } + + return mSystemIdentityCode; + } + /** + * Announcement message type for this message + */ + public AnnouncementType getAnnouncementType() + { + return getAnnouncementType(getMessage()); + } + + /** + * Utility to get announcement message type for the specified message + */ + public static AnnouncementType getAnnouncementType(CorrectedBinaryMessage message) + { + return AnnouncementType.fromValue(message.getInt(ANNOUNCEMENT_TYPE)); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTPreamble.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTPreamble.java new file mode 100644 index 00000000..962c23f6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTPreamble.java @@ -0,0 +1,188 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera; + +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.module.decode.dmr.DMRSyncPattern; +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.standard.Preamble; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera Preamble + */ +public class HyteraXPTPreamble extends Preamble +{ + private static final int[] FREE_REPEATER = new int[]{32, 33, 34, 35}; + private static final int[] PRIORITY_REPEATER = new int[]{36, 37, 38, 39}; + private static final int[] TARGET_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] SOURCE_ADDRESS = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mTargetAddress; + private RadioIdentifier mSourceAddress; + private List 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 HyteraXPTPreamble(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()); + sb.append(" CSBK PREAMBLE FM:").append(getSourceAddress()); + sb.append(" TO:").append(getTargetAddress()); + sb.append(isCSBKPreamble() ? " CSBK" : " DATA"); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + if(isAllChannelsBusy()) + { + sb.append(" ALL REPEATERS BUSY"); + } + else + { + sb.append(" FREE REPEATER:").append(getFreeRepeater()); + } + + if(hasPriorityCall()) + { + sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); + sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Indicates if all free repreaters are busy + */ + public boolean isAllChannelsBusy() + { + return getFreeRepeater() == 0; + } + + + /** + * Free repeater number. + * + * @return repeater, or 0 for all repeaters busy + */ + public int getFreeRepeater() + { + return getMessage().getInt(FREE_REPEATER); + } + + /** + * Indicates if there is a priority call on another repeater channel that users should monitor when their + * talkgroup matches the priority call hashed address + */ + public boolean hasPriorityCall() + { + return getPriorityCallRepeater() > 0; + } + + /** + * Indicates the repeater number that is forwarding a priority call + */ + public int getPriorityCallRepeater() + { + return getMessage().getInt(PRIORITY_REPEATER); + } + + /** + * Hashed address of priority call + */ + public String getPriorityCallHashedAddress() + { + return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase(); + } + + /** + * Target radio address that is either a radio or talkgroup identifier + */ + public IntegerIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + if(isTalkgroupTargetAddress()) + { + mTargetAddress = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + else + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + } + + return mTargetAddress; + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceAddress() + { + if(mSourceAddress == null) + { + mSourceAddress = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceAddress; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceAddress()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTSiteState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTSiteState.java new file mode 100644 index 00000000..62649aeb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraXPTSiteState.java @@ -0,0 +1,276 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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; + +/** + * Hytera XPT - Site State + * + * Note: I've only seen this documented in the patent and haven't yet seen it in the wild. So, I'm not sure if this + * is a CSBK or not. CSBK is the only thing that makes sense, but they've redefined bits 0 and 1 of the message which + * collides with CSBK bits 0 and 1. + */ +public class HyteraXPTSiteState extends CSBKMessage +{ + private static final int[] SEQUENCE_NUMBER = new int[]{0, 1}; + private static final int[] FREE_REPEATER = new int[]{16, 17, 18, 19}; + private static final int[] REPEATER_A_STATE = new int[]{20, 21, 22, 23}; + private static final int[] REPEATER_B_STATE = new int[]{24, 25, 26, 27}; + private static final int[] REPEATER_C_STATE = new int[]{28, 29, 30, 31}; + private static final int[] REPEATER_A_TS0_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] REPEATER_A_TS1_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] REPEATER_B_TS0_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] REPEATER_B_TS1_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] REPEATER_C_TS0_ADDRESS = new int[]{64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] REPEATER_C_TS1_ADDRESS = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private List 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 HyteraXPTSiteState(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Sequence number to indicate the set of site's repeaters that are being transmitted: + * + * SN 0: repeaters 1-3 + * SN 1: repeaters 4-6 + * SN 2: repeaters 7-9 + * + * @return sequence number + */ + public int getSequenceNumber() + { + return getMessage().getInt(SEQUENCE_NUMBER); + } + + /** + * Indicates if all channels of the site are busy. + */ + public boolean isAllChannelsBusy() + { + return getFreeRepeater() == 0; + } + + /** + * Free repeater number to use when both timeslots of this repeater are busy + */ + public int getFreeRepeater() + { + return getMessage().getInt(FREE_REPEATER); + } + + /** + * Repeater number for repeater A field for this message + */ + public int getRepeaterANumber() + { + switch(getSequenceNumber()) + { + case 0: + return 1; + case 1: + return 4; + case 2: + return 7; + } + + return 0; + } + + /** + * Repeater number for repeater B field for this message + */ + public int getRepeaterBNumber() + { + switch(getSequenceNumber()) + { + case 0: + return 2; + case 1: + return 5; + case 2: + return 8; + } + + return 0; + } + + /** + * Repeater number for repeater C field for this message + */ + public int getRepeaterCNumber() + { + switch(getSequenceNumber()) + { + case 0: + return 3; + case 1: + return 6; + case 2: + return 9; + } + + return 0; + } + + /** + * State of Repeater A + */ + public int getRepeaterAStateInfo() + { + return getMessage().getInt(REPEATER_A_STATE); + } + + /** + * State of Repeater B + */ + public int getRepeaterBStateInfo() + { + return getMessage().getInt(REPEATER_B_STATE); + } + + /** + * State of Repeater C + */ + public int getRepeaterCStateInfo() + { + return getMessage().getInt(REPEATER_C_STATE); + } + + /** + * Hashed Address that is currently using Repeater A Timeslot 0 + */ + public String getRepeaterATimeslot0HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_A_TS0_ADDRESS)); + } + + /** + * Hashed Address that is currently using Repeater A Timeslot 1 + */ + public String getRepeaterATimeslot1HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_A_TS1_ADDRESS)); + } + + /** + * Hashed Address that is currently using Repeater B Timeslot 0 + */ + public String getRepeaterBTimeslot0HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_B_TS0_ADDRESS)); + } + + /** + * Hashed Address that is currently using Repeater B Timeslot 1 + */ + public String getRepeaterBTimeslot1HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_B_TS1_ADDRESS)); + } + + /** + * Hashed Address that is currently using Repeater C Timeslot 0 + */ + public String getRepeaterCTimeslot0HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_C_TS0_ADDRESS)); + } + + /** + * Hashed Address that is currently using Repeater C Timeslot 1 + */ + public String getRepeaterCTimeslot1HashedAddress() + { + return String.format("%02X", getMessage().getInt(REPEATER_C_TS1_ADDRESS)); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" HYTERA XPT SITE "); + + if(isAllChannelsBusy()) + { + sb.append(" ALL CHANNELS BUSY"); + } + else + { + sb.append(" FREE:").append(getFreeRepeater()); + } + + //Repeater A + sb.append(" STATE R").append(getRepeaterANumber()).append(":").append(getRepeaterAStateInfo()); + sb.append(" (").append(getRepeaterATimeslot0HashedAddress()).append("/"); + sb.append(getRepeaterATimeslot1HashedAddress()).append(")"); + + //Repeater B + sb.append(" R").append(getRepeaterBNumber()).append(":").append(getRepeaterBStateInfo()); + sb.append(" (").append(getRepeaterBTimeslot0HashedAddress()).append("/"); + sb.append(getRepeaterBTimeslot1HashedAddress()).append(")"); + + //Repeater C + sb.append(" R").append(getRepeaterCNumber()).append(":").append(getRepeaterCStateInfo()); + sb.append(" (").append(getRepeaterCTimeslot0HashedAddress()).append("/"); + sb.append(getRepeaterCTimeslot1HashedAddress()).append(")"); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAloha.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAloha.java new file mode 100644 index 00000000..f5eaefd7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAloha.java @@ -0,0 +1,196 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 io.github.dsheirer.module.decode.dmr.message.type.ServiceFunction; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; +import io.github.dsheirer.module.decode.dmr.message.type.Version; + +import java.util.ArrayList; +import java.util.List; + +/** + * Capacity Plus - Aloha Message + */ +public class CapacityMaxAloha extends CSBKMessage +{ + private static final int[] RESERVED = new int[]{16, 17}; + private static final int SITE_TIMESLOT_SYNCHRONIZATION = 18; + private static final int[] VERSION = new int[]{19, 20, 21}; + private static final int TIMING_OFFSET = 22; + private static final int ACTIVE_NETWORK_CONNECTION_FLAG = 23; + private static final int[] MASK = new int[]{24, 25, 26, 27, 28}; + private static final int[] SERVICE_FUNCTION = new int[]{29, 30}; + private static final int[] N_RAND_WAIT = new int[]{31, 32, 33, 34}; + private static final int REGISTRATION_REQUIRED_FLAG = 35; + private static final int[] BACKOFF = new int[]{36, 37, 38, 39}; + private static final int SYSTEM_IDENTITY_CODE_OFFSET = 40; + private static final int[] RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private SystemIdentityCode mSystemIdentityCode; + private RadioIdentifier mRadioIdentifier; + private List 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 CapacityMaxAloha(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()); + sb.append(" CAPACITY-MAX ALOHA"); + + if(hasRadioIdentifier()) + { + sb.append(" TO:").append(getRadioIdentifier()); + } + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + if(hasActiveNetworkConnection()) + { + sb.append(" NET-CONNECTED"); + } + else + { + sb.append(" NET-DISCONNECTED"); + } + + sb.append(" SERVICES:").append(getServiceFunction()); + + sb.append(" ETSI VER:").append(getVersion()); + sb.append(" MASK:").append(getMask()); + + if(getSystemIdentityCode().getPAR().isMultipleControlChannels()) + { + sb.append(" ").append(getSystemIdentityCode().getPAR()); + } + + sb.append(" ").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Services provided by the control channel. + */ + public ServiceFunction getServiceFunction() + { + return ServiceFunction.fromValue(getMessage().getInt(SERVICE_FUNCTION)); + } + + /** + * Mobile subscriber ID masking value. See: 102 361-4 p6.1.3 + */ + public int getMask() + { + return getMessage().getInt(MASK); + } + + public boolean hasActiveNetworkConnection() + { + return getMessage().get(ACTIVE_NETWORK_CONNECTION_FLAG); + } + + /** + * DMR Tier III ETSI 102 361-4 ICD Version Number supported by this system + */ + public Version getVersion() + { + return Version.fromValue(getMessage().getInt(VERSION)); + } + + /** + * Acknowledged radio identifier + */ + public RadioIdentifier getRadioIdentifier() + { + if(mRadioIdentifier == null) + { + mRadioIdentifier = DMRRadio.createTo(getMessage().getInt(RADIO)); + } + + return mRadioIdentifier; + } + + public boolean hasRadioIdentifier() + { + return getMessage().getInt(RADIO) != 0; + } + + /** + * System Identity Code structure + */ + public SystemIdentityCode getSystemIdentityCode() + { + if(mSystemIdentityCode == null) + { + mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, true); + } + + return mSystemIdentityCode; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasRadioIdentifier()) + { + mIdentifiers.add(getRadioIdentifier()); + } + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusCSBKO_60.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusCSBKO_60.java new file mode 100644 index 00000000..272fd7ce --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusCSBKO_60.java @@ -0,0 +1,74 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.module.decode.dmr.DMRSyncPattern; +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.Collections; +import java.util.List; + +/** + * Capacity Plus - CSBK 60 Message + */ +public class CapacityPlusCSBKO_60 extends CSBKMessage +{ + /** + * 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 CapacityPlusCSBKO_60(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()); + sb.append(" CAP+ *UNKNOWN* CSBK:60"); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowAnnouncement.java new file mode 100644 index 00000000..2fd54bfb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowAnnouncement.java @@ -0,0 +1,162 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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; + +/** + * Capacity+ Window Announcement for Enhanced GPS/Data Revert Channel + * + * The window size dictates how many Windows happen within each data super frame. You can infer the window size + * by monitoring the maximum observed window value before rollover to zero as follows: + * + * Window Size: Number of Windows (hex) for 30-Second Data Super Frames + * 5: 100 (0x52) + * 6: 83 (0x53) + * 7: 71 (0x47) + * 8: 62 (0x3E) + * 9: 55 (0x37) + * 10: 50 (0x32) + * + * Window Size: Number of Windows (hex) for 2-Minute Data Super Frames + * 1: 125 (0x7D) + * 2: 62 (0x3E) + * + * Field UNKNOWN_2 may be reserved for a radio identifier that the target radio should transmit to. + */ +public class CapacityPlusDataRevertWindowAnnouncement extends CSBKMessage +{ + private static final int[] RESERVED = new int[]{16, 17, 18, 19, 20, 21, 22, 23,}; //Probably reserved for 32-bit addressing + private static final int[] TARGET_RADIO = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] WINDOW = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] SUPER_FRAME = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] UNKNOWN_1 = new int[]{56, 57, 58, 59, 60, 61, 62, 63}; //Always 0xFC + private static final int[] UNKNOWN_2 = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; //Always 0x0000 + + private RadioIdentifier mTargetRadio; + private List 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 CapacityPlusDataRevertWindowAnnouncement(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()); + sb.append(" MOTOROLA CAP+ ENHANCED DATA REVERT ANNOUNCEMENT"); + sb.append(" WINDOW:").append(getSuperFrame()).append(".").append(getWindow()); + + if(hasTargetRadio()) + { + sb.append(" RESERVED FOR RADIO:").append(getTargetRadio()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Indicates if there is a target radio specified in this message + */ + public boolean hasTargetRadio() + { + return getMessage().getInt(TARGET_RADIO) > 0; + } + + /** + * Optional target radio identifier that the announced window is reserved for + * @return radio identifier or NULL + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null && hasTargetRadio()) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_RADIO)); + } + + return mTargetRadio; + } + + /** + * Current Super Frame Number + * + * @return 0-15 + */ + public int getSuperFrame() + { + return getMessage().getInt(SUPER_FRAME); + } + + /** + * Current Window Number + * + * @return current window number, values range from 0 to 125 depending on repeater configuration + */ + public int getWindow() + { + return getMessage().getInt(WINDOW); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasTargetRadio()) + { + mIdentifiers.add(getTargetRadio()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowGrant.java new file mode 100644 index 00000000..77987a31 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusDataRevertWindowGrant.java @@ -0,0 +1,119 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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+ Enhanced Data Revert Channel - Window Grant + * + * This appears to be a grant to an individual user to transmit data in a specified super frame and window. + */ +public class CapacityPlusDataRevertWindowGrant extends CSBKMessage +{ + //Note: bits 16-23 may be reserved for 24-bit target address, however Cap+ radio IDs are limited to 16-bit values. + private static final int[] TARGET_ADDRESS = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] WINDOW = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] SUPER_FRAME = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + + private RadioIdentifier mTargetAddress; + private List 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 CapacityPlusDataRevertWindowGrant(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()); + sb.append(" MOTOROLA CAP+ ENHANCED DATA REVERT GRANT TO:").append(getTargetAddress()); + sb.append(" TRANSMIT IN WINDOW:").append(getSuperFrame()).append(".").append(getWindow()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + public RadioIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetAddress; + } + + /** + * Super frame (window) in which the target mobile should transmit their data + */ + public int getSuperFrame() + { + return getMessage().getInt(SUPER_FRAME); + } + + /** + * Data frame window within the super frame in which the target mobile should transmit their data + */ + public int getWindow() + { + return getMessage().getInt(WINDOW); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusNeighbors.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusNeighbors.java new file mode 100644 index 00000000..76be2dd3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusNeighbors.java @@ -0,0 +1,342 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.DMRSite; +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 io.github.dsheirer.module.decode.dmr.message.type.LCSS; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Capacity Plus - Neighbor Sites + */ +public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] LC_START_STOP = new int[]{16, 17}; + private static final int TIMESLOT = 18; + private static final int[] REST_REPEATER = new int[]{19, 20, 21, 22}; + private static final int[] REST_TIMESLOT = new int[]{23}; + private static final int ASYNC = 24; + private static final int[] SITE = new int[]{25, 26, 27, 28}; + private static final int[] NEIGHBOR_COUNT = new int[]{29, 30, 31}; + private static final int[] NEIGHBOR_1_SITE = new int[]{32, 33, 34, 35}; + private static final int[] NEIGHBOR_1_REST = new int[]{36, 37, 38, 39}; + private static final int[] NEIGHBOR_2_SITE = new int[]{40, 41, 42, 43}; + private static final int[] NEIGHBOR_2_REST = new int[]{44, 45, 46, 47}; + private static final int[] NEIGHBOR_3_SITE = new int[]{48, 49, 50, 51}; + private static final int[] NEIGHBOR_3_REST = new int[]{52, 53, 54, 55}; + private static final int[] NEIGHBOR_4_SITE = new int[]{56, 57, 58, 59}; + private static final int[] NEIGHBOR_4_REST = new int[]{60, 61, 62, 63}; + private static final int[] NEIGHBOR_5_SITE = new int[]{64, 65, 66, 67}; + private static final int[] NEIGHBOR_5_REST = new int[]{68, 69, 70, 71}; + private static final int[] NEIGHBOR_6_SITE = new int[]{72, 73, 74, 75}; + private static final int[] NEIGHBOR_6_REST = new int[]{76, 77, 78, 79}; + + + private DMRLogicalChannel mRestChannel; + private DMRSite mSite; + private List 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 CapacityPlusNeighbors(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()); + sb.append(" MOTOROLA CAP+ SITE:").append(getSite()); + sb.append(" REST ").append(getRestChannel()); + sb.append(" FL:").append(getLCSS()); + + if(hasNeighbor(1)) + { + sb.append(" NEIGHBORS SITE:").append(getNeighbor1Site()); + sb.append("-REST:").append(getNeighbor1Rest()); + + if(hasNeighbor(2)) + { + sb.append(" SITE:").append(getNeighbor2Site()); + sb.append("-REST:").append(getNeighbor2Rest()); + + if(hasNeighbor(3)) + { + sb.append(" SITE:").append(getNeighbor3Site()); + sb.append("-REST:").append(getNeighbor3Rest()); + + if(hasNeighbor(4)) + { + sb.append(" SITE:").append(getNeighbor4Site()); + sb.append("-REST:").append(getNeighbor4Rest()); + + if(hasNeighbor(5)) + { + sb.append(" SITE:").append(getNeighbor5Site()); + sb.append("-REST:").append(getNeighbor5Rest()); + + if(hasNeighbor(6)) + { + sb.append(" SITE:").append(getNeighbor6Site()); + sb.append("-REST:").append(getNeighbor6Rest()); + } + } + } + } + } + } + + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + public boolean hasNeighbor(int neighbor) + { + return neighbor <= getNeighborCount(); + } + + /** + * Number of neighbors reported + */ + public int getNeighborCount() + { + return getMessage().getInt(NEIGHBOR_COUNT); + } + + + /** + * Neighbor 1 Site + */ + public int getNeighbor1Site() + { + return getMessage().getInt(NEIGHBOR_1_SITE); + } + + /** + * Neighbor 1 Current Rest Channel + */ + public int getNeighbor1Rest() + { + return getMessage().getInt(NEIGHBOR_1_REST); + } + + /** + * Neighbor 2 Site + */ + public int getNeighbor2Site() + { + return getMessage().getInt(NEIGHBOR_2_SITE); + } + + /** + * Neighbor 2 Current Rest Channel + */ + public int getNeighbor2Rest() + { + return getMessage().getInt(NEIGHBOR_2_REST); + } + + /** + * Neighbor 3 Site + */ + public int getNeighbor3Site() + { + return getMessage().getInt(NEIGHBOR_3_SITE); + } + + /** + * Neighbor 3 Current Rest Channel + */ + public int getNeighbor3Rest() + { + return getMessage().getInt(NEIGHBOR_3_REST); + } + + /** + * Neighbor 4 Site + */ + public int getNeighbor4Site() + { + return getMessage().getInt(NEIGHBOR_4_SITE); + } + + /** + * Neighbor 4 Current Rest Channel + */ + public int getNeighbor4Rest() + { + return getMessage().getInt(NEIGHBOR_4_REST); + } + + /** + * Neighbor 5 Site + */ + public int getNeighbor5Site() + { + return getMessage().getInt(NEIGHBOR_5_SITE); + } + + /** + * Neighbor 5 Current Rest Channel + */ + public int getNeighbor5Rest() + { + return getMessage().getInt(NEIGHBOR_5_REST); + } + + /** + * Neighbor 6 Site + */ + public int getNeighbor6Site() + { + return getMessage().getInt(NEIGHBOR_6_SITE); + } + + /** + * Neighbor 6 Current Rest Channel + */ + public int getNeighbor6Rest() + { + return getMessage().getInt(NEIGHBOR_6_REST); + } + + /** + * This site number + * + * @return + */ + public DMRSite getSite() + { + if(mSite == null) + { + mSite = DMRSite.create(getMessage().getInt(SITE)); + } + + return mSite; + } + + /** + * Indicates if the site uses period beacons or asynchronous beacons on the rest channel. + */ + public String getBeacon() + { + return getMessage().get(ASYNC) ? "ASYNC" : "PERIODIC"; + } + + /** + * Message Fragment indicator + */ + public LCSS getLCSS() + { + return LCSS.fromValue(getMessage().getInt(LC_START_STOP)); + } + + /** + * Rest repeater + */ + public int getRestRepeater() + { + return getMessage().getInt(REST_REPEATER) + 1; + } + + /** + * Rest timeslot + * @return timeslot 1 or 2 + */ + public int getRestTimeslot() + { + return getMessage().getInt(REST_TIMESLOT) + 1; + } + + /** + * DMR Channel + */ + public DMRLogicalChannel getRestChannel() + { + if(mRestChannel == null) + { + mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); + } + + return mRestChannel; + } + + @Override + public int[] getLogicalTimeslotNumbers() + { + return getRestChannel().getLSNArray(); + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) + { + if(timeslotFrequency.getNumber() == getRestChannel().getLogicalSlotNumber()) + { + getRestChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getRestChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusPreamble.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusPreamble.java new file mode 100644 index 00000000..3f3daaae --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusPreamble.java @@ -0,0 +1,159 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.module.decode.dmr.DMRSyncPattern; +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.standard.Preamble; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Capacity Plus - Preamble + */ +public class CapacityPlusPreamble extends Preamble +{ + private static final int RADIO_TALKGROUP_FLAG = 17; + private static final int[] BLOCKS_TO_FOLLOW = new int[]{18, 19, 20, 21, 22}; + private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] UNKNOWN_2 = new int[]{32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] TARGET_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] UNKNOWN_3 = new int[]{56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] SOURCE_ADDRESS = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mTargetAddress; + private RadioIdentifier mSourceAddress; + private List 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 CapacityPlusPreamble(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()); + sb.append(" MOTOROLA CAP+").append(isCSBKPreamble() ? " CSBK" : " DATA"); + sb.append(" PREAMBLE FM:").append(getSourceAddress()); + sb.append(" TO:").append(getTargetAddress()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + sb.append(" UNK1:").append(getUnknown1()); + sb.append(" UNK2:").append(getUnknown2()); + sb.append(" UNK3:").append(getUnknown3()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + public String getUnknown1() + { + return getMessage().getHex(UNKNOWN_1, 2); + } + + public String getUnknown2() + { + return getMessage().getHex(UNKNOWN_2, 2); + } + + public String getUnknown3() + { + return getMessage().getHex(UNKNOWN_3, 2); + } + + /** + * Number of data blocks that will follow this preamble + */ + public int getBlocksToFollow() + { + return getMessage().getInt(BLOCKS_TO_FOLLOW); + } + + /** + * Target radio address that is either a radio or talkgroup identifier + */ + public IntegerIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mTargetAddress = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + else + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + } + + return mTargetAddress; + } + + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceAddress() + { + if(mSourceAddress == null) + { + mSourceAddress = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceAddress; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceAddress()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSystemStatus.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSystemStatus.java new file mode 100644 index 00000000..b507cad4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSystemStatus.java @@ -0,0 +1,158 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; +import io.github.dsheirer.module.decode.dmr.message.type.LCSS; + +import java.util.ArrayList; +import java.util.List; + +/** + * Capacity+ System Status CSBKO=62 Message + */ +public class CapacityPlusSystemStatus extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] FRAGMENT_INDICATOR = new int[]{16, 17}; +// private static final int TIMESLOT = 18; + private static final int[] REST_REPEATER = new int[]{19, 20, 21, 22}; + private static final int[] REST_TIMESLOT = new int[]{23}; + + private DMRLogicalChannel mRestChannel; + private List 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 CapacityPlusSystemStatus(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()); + sb.append(" MOTOROLA CAP+ SYSTEM STATUS ").append(getFragmentIndicator()); + sb.append(" REST ").append(getRestChannel()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Fragment indicator for system status message values that are fragmented across multiple system status + * messages. + */ + public LCSS getFragmentIndicator() + { + return LCSS.fromValue(getMessage().getInt(FRAGMENT_INDICATOR)); + } + + /** + * Current rest channel for this site. + */ + public DMRLogicalChannel getRestChannel() + { + if(mRestChannel == null) + { + mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); + } + + return mRestChannel; + } + + /** + * Rest Channel Repeater + */ + public int getRestRepeater() + { + return getMessage().getInt(REST_REPEATER) + 1; + } + + /** + * Rest Channel Timeslot + * @return 1 or 2 + */ + public int getRestTimeslot() + { + return getMessage().getInt(REST_TIMESLOT) + 1; + } + + /** + * Logical slot numbers that require slot to frequency mappings. + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + return getRestChannel().getLSNArray(); + } + + /** + * Applies logical slot number to frequency mapping. + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) + { + if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + getRestChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getRestChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusCSBKO_16.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusCSBKO_16.java new file mode 100644 index 00000000..6ded7945 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusCSBKO_16.java @@ -0,0 +1,129 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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; + +/** + * Connect Plus - CSBK 16 Message + */ +public class ConnectPlusCSBKO_16 extends CSBKMessage +{ + private static final int[] SOURCE_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] TARGET_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mTargetRadio; + + /** + * 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 ConnectPlusCSBKO_16(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()); + sb.append(" CON+ CSBKO:16 FM:").append(getSourceRadio()); + sb.append(" TO:").append(getTargetRadio()); + sb.append(" UNK:").append(getUnknown()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Unknown Field + */ + public String getUnknown() + { + return String.format("%04X", getMessage().getInt(UNKNOWN)); + } + + /** + * Source Radio ID + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + + /** + * Target Radio ID + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataChannelGrant.java new file mode 100644 index 00000000..64f14961 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataChannelGrant.java @@ -0,0 +1,173 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.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 Connect Plus - Data Channel Grant + */ +public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] REPEATER = new int[]{40, 41, 42, 43}; + private static final int[] CHANNEL_GRANT_TIMESLOT = new int[]{44}; + + //Analysis: this field correlates to UNKNOWN_FIELD_1(bits: 40-48) in ConnectPlusTerminateChannelGrant. + private static final int[] UNKNOWN_FIELD = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + + private RadioIdentifier mTargetRadio; + private DMRLogicalChannel mDMRLogicalChannel; + private List 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 ConnectPlusDataChannelGrant(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()); + sb.append(" CSBK ").append(getVendor()); + sb.append(" DATA CHANNEL GRANT TO:").append(getTargetRadio()); + sb.append(" ").append(getChannel()); + sb.append(" UNK:").append(getUnknownField()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Target radio address + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + /** + * Unknown field + */ + public int getUnknownField() + { + return getMessage().getInt(UNKNOWN_FIELD); + } + + /** + * Channel grant repeater number + */ + public int getRepeater() + { + return getMessage().getInt(REPEATER); + } + + /** + * Channel grant timeslot + * @return 1 or 2 + */ + public int getChannelGrantTimeslot() + { + return getMessage().getInt(CHANNEL_GRANT_TIMESLOT) + 1; + } + + /** + * DMR Channel + */ + public DMRLogicalChannel getChannel() + { + if(mDMRLogicalChannel == null) + { + mDMRLogicalChannel = new DMRLogicalChannel(getRepeater(), getChannelGrantTimeslot()); + } + + return mDMRLogicalChannel; + } + + @Override + public int[] getLogicalTimeslotNumbers() + { + return getChannel().getLSNArray(); + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(timeslotFrequency.getNumber() == getChannel().getLogicalSlotNumber()) + { + getChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + mIdentifiers.add(getChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowAnnouncement.java new file mode 100644 index 00000000..6545441b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowAnnouncement.java @@ -0,0 +1,137 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 Connect Plus - Fast GPS or Enhanced GPS / Data Revert Window Announcement. + */ +public class ConnectPlusDataRevertWindowAnnouncement extends CSBKMessage +{ + private static final int[] WINDOW = new int[]{16, 17, 18, 19, 20, 21, 22}; + private static final int[] SUPERFRAME = new int[]{24, 25, 26, 27}; + private static final int[] REPEATER = new int[]{28, 29, 30, 31}; //This may be the repeater number + private static final int[] TARGET_RADIO = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55}; + + private List mIdentifiers; + private RadioIdentifier mTargetRadio; + + /** + * 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 ConnectPlusDataRevertWindowAnnouncement(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()); + sb.append(" CON+ ENHANCED DATA REVERT CHANNEL ANNOUNCE WINDOW:"); + sb.append(getSuperFrame()).append(".").append(getWindow()); + if(hasTargetRadio()) + { + sb.append(" RESERVED FOR:").append(getTargetRadio()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Radio identifier that this window is reserved for. + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_RADIO)); + } + + return mTargetRadio; + } + + /** + * Indicates if this GPS transmit window is reserved for a specific radio identifier. + */ + public boolean hasTargetRadio() + { + return getMessage().getInt(TARGET_RADIO) > 0; + } + + /** + * GPS Window Super Frame + */ + public int getSuperFrame() + { + return getMessage().getInt(SUPERFRAME); + } + + /** + * GPS Window within a super frame + */ + public int getWindow() + { + return getMessage().getInt(WINDOW); + } + + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasTargetRadio()) + { + mIdentifiers.add(getTargetRadio()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowGrant.java new file mode 100644 index 00000000..5cb65dee --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusDataRevertWindowGrant.java @@ -0,0 +1,122 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 Connect Plus - Enhanced Data Revert - Window Grant + */ +public class ConnectPlusDataRevertWindowGrant extends CSBKMessage +{ + private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SUPERFRAME = new int[]{48, 49, 50, 51}; + + //TODO: this may not be correct + private static final int[] WINDOW = new int[]{70, 71, 72, 73, 74}; + + private List mIdentifiers; + private RadioIdentifier mTargetRadio; + + /** + * 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 ConnectPlusDataRevertWindowGrant(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()); + sb.append(" CON+ ENHANCED DATA REVERT GRANT TO:").append(getTargetRadio()); + sb.append(" TRANSMIT IN WINDOW:").append(getSuperFrame()).append(".").append(getWindow()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * GPS Window Super Frame + */ + public int getSuperFrame() + { + return getMessage().getInt(SUPERFRAME); + } + + /** + * GPS Window within a super frame + */ + public int getWindow() + { + return getMessage().getInt(WINDOW); + } + + /** + * Current radio user for the channel + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createFrom(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusNeighborReport.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusNeighborReport.java new file mode 100644 index 00000000..f7bbab70 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusNeighborReport.java @@ -0,0 +1,143 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola; + +import com.google.common.base.Joiner; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.site.SiteIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; +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 Connect Plus - Neighbor Report + */ +public class ConnectPlusNeighborReport extends CSBKMessage +{ + private static final int SITE_ARRAY_START = 16; + private static final int[] UNKNOWN = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private List mNeighbors; + private List 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 ConnectPlusNeighborReport(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()); + sb.append(" CON+ NEIGHBORS:"); + List neighbors = getNeighbors(); + + if(neighbors.isEmpty()) + { + sb.append("NONE"); + } + else + { + sb.append(Joiner.on(",").join(getNeighbors())); + } + + sb.append(" UNK:").append(getUnknownField()); +// sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + public String getUnknownField() + { + return getMessage().getHex(UNKNOWN, 6); + } + + /** + * List of neighbor site identifiers + */ + public List getNeighbors() + { + if(mNeighbors == null) + { + mNeighbors = new ArrayList<>(); + + for(int x = 0; x <= 5; x++) + { + int site = getSite(x); + + if(site > 0) + { + mNeighbors.add(DMRSite.create(site)); + } + } + } + + return mNeighbors; + } + + /** + * Retrieves the specified neighbor site value. + * + * @param index 0 - 6 + * @return site id value. A value of zero indicates the field is empty. + */ + private int getSite(int index) + { + int start = SITE_ARRAY_START + (index * 8); + int end = start + 7; + return getMessage().getInt(start, end); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.addAll(getNeighbors()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusOTAAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusOTAAnnouncement.java new file mode 100644 index 00000000..57f950c0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusOTAAnnouncement.java @@ -0,0 +1,163 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; +import io.github.dsheirer.module.decode.ip.xcmp.XCMPMessageType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Connect Plus - Over The Air (OTA) Reprogramming Announcement + * + * Note: there are at least 2 file types that can be transferred: + * 1. Network Frequency File + * 2. Option Board Firmware + */ +public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] MESSAGE_TYPE = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] VERSION = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] UNKNOWN = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63}; + private static final int[] DATA_REPEATER = new int[]{64, 65, 66, 67}; + private static final int[] DATA_TIMESLOT = new int[]{68}; + + private DMRLogicalChannel mDataChannel; + private List 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 ConnectPlusOTAAnnouncement(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()); + sb.append(" CON+ ANNOUNCE OTA ").append(getMessageType()); + sb.append(" VER:").append(getMessageVersion()); + sb.append(" AVAILABLE ON ").append(getDataChannel()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + public XCMPMessageType getMessageType() + { + return XCMPMessageType.fromValue(getMessage().getInt(MESSAGE_TYPE)); + } + + public int getMessageVersion() + { + return getMessage().getInt(VERSION); + } + + /** + * Data repeater number + */ + public int getDataRepeater() + { + return getMessage().getInt(DATA_REPEATER); + } + + /** + * Data Timeslot + * @return 1 or 2 + */ + public int getDataTimeslot() + { + return getMessage().getInt(DATA_TIMESLOT) + 1; + } + + /** + * DMR Channel where the data is available + */ + public DMRLogicalChannel getDataChannel() + { + if(mDataChannel == null) + { + mDataChannel = new DMRLogicalChannel(getDataRepeater(), getDataTimeslot()); + } + + return mDataChannel; + } + + @Override + public int[] getLogicalTimeslotNumbers() + { + return getDataChannel().getLSNArray(); + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) + { + if(timeslotFrequency.getNumber() == getDataChannel().getLogicalSlotNumber()) + { + getDataChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDataChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationRequest.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationRequest.java new file mode 100644 index 00000000..920110d8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationRequest.java @@ -0,0 +1,129 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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; + +/** + * Connect Plus - Registration Request + */ +public class ConnectPlusRegistrationRequest extends CSBKMessage +{ + private static final int[] SOURCE_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] TARGET_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71}; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mTargetRadio; + + /** + * 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 ConnectPlusRegistrationRequest(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()); + sb.append(" CON+ REGISTRATION REQUEST FM:").append(getSourceRadio()); + sb.append(" TO:").append(getTargetRadio()); + sb.append(" UNK:").append(getUnknown()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Unknown + */ + public int getUnknown() + { + return getMessage().getInt(UNKNOWN); + } + + /** + * Source Radio ID + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + + /** + * Target Radio ID + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationResponse.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationResponse.java new file mode 100644 index 00000000..0675c5e1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusRegistrationResponse.java @@ -0,0 +1,129 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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; + +/** + * Connect Plus - Registration Response + */ +public class ConnectPlusRegistrationResponse extends CSBKMessage +{ + private static final int[] SOURCE_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] TARGET_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71}; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mTargetRadio; + + /** + * 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 ConnectPlusRegistrationResponse(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()); + sb.append(" CON+ REGISTRATION RESPONSE FM:").append(getSourceRadio()); + sb.append(" TO:").append(getTargetRadio()); + sb.append(" UNK:").append(getUnknown()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Unknown + */ + public int getUnknown() + { + return getMessage().getInt(UNKNOWN); + } + + /** + * Source Radio ID + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + + /** + * Target Radio ID + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTalkgroupAffiliation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTalkgroupAffiliation.java new file mode 100644 index 00000000..e65f11ce --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTalkgroupAffiliation.java @@ -0,0 +1,131 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.List; + +/** + * Connect Plus - Radio:Talkgroup Affiliation + */ +public class ConnectPlusTalkgroupAffiliation extends CSBKMessage +{ + private static final int[] RADIO = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] TALKGROUP = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71}; + + private List mIdentifiers; + private RadioIdentifier mRadio; + private TalkgroupIdentifier mTalkgroup; + + /** + * 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 ConnectPlusTalkgroupAffiliation(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()); + sb.append(" CON+ AFFILIATE RADIO:").append(getRadio()); + sb.append(" TO TALKGROUP:").append(getTalkgroup()); + sb.append(" UNK:").append(getUnknown()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Unknown + */ + public int getUnknown() + { + return getMessage().getInt(UNKNOWN); + } + + /** + * Radio ID that is being affiliated + */ + public RadioIdentifier getRadio() + { + if(mRadio == null) + { + mRadio = DMRRadio.createFrom(getMessage().getInt(RADIO)); + } + + return mRadio; + } + + /** + * Talkgroup that the radio is affiliated to + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getRadio()); + mIdentifiers.add(getTalkgroup()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTerminateChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTerminateChannelGrant.java new file mode 100644 index 00000000..c99315fd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusTerminateChannelGrant.java @@ -0,0 +1,155 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 Connect Plus - Terminate Channel Grant + */ +public class ConnectPlusTerminateChannelGrant extends CSBKMessage +{ + private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39}; + + //Analysis: this field correlates to UNKNOWN_FIELD(bits: 48-55) in ConnectPlusDataChannelGrant. + private static final int[] UNKNOWN_FIELD_1 = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + + private static final int[] UNKNOWN_FIELD_2 = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] UNKNOWN_FIELD_3 = new int[]{56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] UNKNOWN_FIELD_4 = new int[]{64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] UNKNOWN_FIELD_5 = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private RadioIdentifier mTargetRadio; + private List 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 ConnectPlusTerminateChannelGrant(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()); + sb.append(" CSBK ").append(getVendor()); + sb.append(" TERMINATE CHANNEL GRANT TO:").append(getTargetRadio()); + sb.append(" U1:").append(getUnknownField1()); + sb.append(" U2:").append(getUnknownField2()); + sb.append(" U3:").append(getUnknownField3()); + sb.append(" U4:").append(getUnknownField4()); + sb.append(" U5:").append(getUnknownField5()); + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Target radio address + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + /** + * Unknown field + */ + public int getUnknownField1() + { + return getMessage().getInt(UNKNOWN_FIELD_1); + } + + /** + * Unknown field + */ + public int getUnknownField2() + { + return getMessage().getInt(UNKNOWN_FIELD_2); + } + + /** + * Unknown field + */ + public int getUnknownField3() + { + return getMessage().getInt(UNKNOWN_FIELD_3); + } + + /** + * Unknown field + */ + public int getUnknownField4() + { + return getMessage().getInt(UNKNOWN_FIELD_4); + } + + /** + * Unknown field + */ + public int getUnknownField5() + { + return getMessage().getInt(UNKNOWN_FIELD_5); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusVoiceChannelUser.java new file mode 100644 index 00000000..316b71a0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/ConnectPlusVoiceChannelUser.java @@ -0,0 +1,189 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.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.DMRLogicalChannel; +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.List; + +/** + * Motorola Connect Plus - Voice Channel User + */ +public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] SOURCE_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] TRAFFIC_CHANNEL_REPEATER = new int[]{64, 65, 66, 67}; + private static final int[] TRAFFIC_CHANNEL_TIMESLOT = new int[]{68}; + private static final int[] UNKNOWN_FIELD = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private RadioIdentifier mRadio; + private TalkgroupIdentifier mTalkgroup; + private DMRLogicalChannel mDMRLogicalChannel; + private List 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 ConnectPlusVoiceChannelUser(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()); + sb.append(" CON+ VOICE CHANNEL USER FM:").append(getRadio()); + sb.append(" TO:").append(getTalkgroup()); + sb.append(" ").append(getChannel()); + sb.append(" UNK:").append(getUnknownField()); + return sb.toString(); + } + + /** + * Source radio address + */ + public RadioIdentifier getRadio() + { + if(mRadio == null) + { + mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mRadio; + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS)); + } + + return mTalkgroup; + } + + /** + * Unknown field + */ + public int getUnknownField() + { + return getMessage().getInt(UNKNOWN_FIELD); + } + + /** + * Traffic channel repeater + */ + public int getTrafficChannelRepeater() + { + return getMessage().getInt(TRAFFIC_CHANNEL_REPEATER); + } + + /** + * Traffic channel timeslot + * @return 1 or 2 + */ + public int getTrafficChannelTimeslot() + { + return getMessage().getInt(TRAFFIC_CHANNEL_TIMESLOT) + 1; + } + + /** + * DMR Channel + */ + public DMRLogicalChannel getChannel() + { + if(mDMRLogicalChannel == null) + { + mDMRLogicalChannel = new DMRLogicalChannel(getTrafficChannelRepeater(), getTrafficChannelTimeslot()); + } + + return mDMRLogicalChannel; + } + + @Override + public int[] getLogicalTimeslotNumbers() + { + return getChannel().getLSNArray(); + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(timeslotFrequency.getNumber() == getChannel().getLogicalSlotNumber()) + { + getChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + mIdentifiers.add(getChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Aloha.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Aloha.java new file mode 100644 index 00000000..3991ae3f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Aloha.java @@ -0,0 +1,196 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 io.github.dsheirer.module.decode.dmr.message.type.ServiceFunction; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; +import io.github.dsheirer.module.decode.dmr.message.type.Version; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Aloha Message + */ +public class Aloha extends CSBKMessage +{ + private static final int[] RESERVED = new int[]{16, 17}; + private static final int SITE_TIMESLOT_SYNCHRONIZATION = 18; + private static final int[] VERSION = new int[]{19, 20, 21}; + private static final int TIMING_OFFSET = 22; + private static final int ACTIVE_NETWORK_CONNECTION_FLAG = 23; + private static final int[] MASK = new int[]{24, 25, 26, 27, 28}; + private static final int[] SERVICE_FUNCTION = new int[]{29, 30}; + private static final int[] N_RAND_WAIT = new int[]{31, 32, 33, 34}; + private static final int REGISTRATION_REQUIRED_FLAG = 35; + private static final int[] BACKOFF = new int[]{36, 37, 38, 39}; + private static final int SYSTEM_IDENTITY_CODE_OFFSET = 40; + private static final int[] RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private SystemIdentityCode mSystemIdentityCode; + private RadioIdentifier mRadioIdentifier; + private List 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 Aloha(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()); + sb.append(" ALOHA"); + + if(hasRadioIdentifier()) + { + sb.append(" TO:").append(getRadioIdentifier()); + } + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + if(hasActiveNetworkConnection()) + { + sb.append(" NET-CONNECTED"); + } + else + { + sb.append(" NET-DISCONNECTED"); + } + + sb.append(" SERVICES:").append(getServiceFunction()); + + sb.append(" ETSI VER:").append(getVersion()); + sb.append(" MASK:").append(getMask()); + + if(getSystemIdentityCode().getPAR().isMultipleControlChannels()) + { + sb.append(" ").append(getSystemIdentityCode().getPAR()); + } + + sb.append(" ").append(getMessage().toHexString()); + + return sb.toString(); + } + + /** + * Services provided by the control channel. + */ + public ServiceFunction getServiceFunction() + { + return ServiceFunction.fromValue(getMessage().getInt(SERVICE_FUNCTION)); + } + + /** + * Mobile subscriber ID masking value. See: 102 361-4 p6.1.3 + */ + public int getMask() + { + return getMessage().getInt(MASK); + } + + public boolean hasActiveNetworkConnection() + { + return getMessage().get(ACTIVE_NETWORK_CONNECTION_FLAG); + } + + /** + * DMR Tier III ETSI 102 361-4 ICD Version Number supported by this system + */ + public Version getVersion() + { + return Version.fromValue(getMessage().getInt(VERSION)); + } + + /** + * Acknowledged radio identifier + */ + public RadioIdentifier getRadioIdentifier() + { + if(mRadioIdentifier == null) + { + mRadioIdentifier = DMRRadio.createTo(getMessage().getInt(RADIO)); + } + + return mRadioIdentifier; + } + + public boolean hasRadioIdentifier() + { + return getMessage().getInt(RADIO) != 0; + } + + /** + * System Identity Code structure + */ + public SystemIdentityCode getSystemIdentityCode() + { + if(mSystemIdentityCode == null) + { + mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, true); + } + + return mSystemIdentityCode; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasRadioIdentifier()) + { + mIdentifiers.add(getRadioIdentifier()); + } + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java new file mode 100644 index 00000000..236c0e68 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java @@ -0,0 +1,265 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard; + +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.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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 io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Clear Channel (return to control channel) + */ +public class Clear 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_FLAG = 31; + protected static final int[] DESTINATION = 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}; + protected static final int[] SOURCE = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private DMRChannel mChannel; + private AbsoluteChannelParameters mAbsoluteChannelParameters; + private RadioIdentifier mSourceRadio; + private IntegerIdentifier mDestinationId; + private List mIdentifiers; + + /** + * Constructs a single-block CSBK 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 Clear(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock continuation containing absolute channel parameters + */ + public Clear(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + this(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, + getMoveToTimeslot()); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" CLEAR - RETURN TO ").append(getMoveToChannel()); + sb.append(" FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationId()); + return sb.toString(); + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + public IntegerIdentifier getDestinationId() + { + if(mDestinationId == null) + { + if(getMessage().get(TALKGROUP_FLAG)) + { + mDestinationId = DMRTalkgroup.create(getMessage().getInt(DESTINATION)); + } + else + { + mDestinationId = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + } + + return mDestinationId; + } + + + /** + * Timeslot to use for the channel grant + */ + private int getMoveToTimeslot() + { + return 1; //Back to the control channel timeslot + } + + /** + * Channel number to move to + */ + private int getMoveToChannelNumber() + { + return getMessage().getInt(CHANNEL_NUMBER); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + /** + * DMR Channel and Timeslot to move to + */ + public DMRChannel getMoveToChannel() + { + if(hasAbsoluteChannelParameters()) + { + return mAbsoluteChannelParameters.getChannel(); + } + + if(mChannel == null) + { + mChannel = new DMRLogicalChannel(getMoveToChannelNumber(), getMoveToTimeslot()); + } + + return mChannel; + } + + /** + * Logical Slot Number(s) for channels contained in this message + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + if(getMoveToChannel() instanceof DMRLogicalChannel) + { + return ((DMRLogicalChannel)getMoveToChannel()).getLSNArray(); + } + + return new int[0]; + } + + /** + * Applies the timeslot frequency lookup information to channels contained in this message + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(getMoveToChannel() instanceof DMRLogicalChannel) + { + DMRLogicalChannel channel = (DMRLogicalChannel)getMoveToChannel(); + + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + channel.setTimeslotFrequency(timeslotFrequency); + } + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getMoveToChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationId()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/MoveTSCC.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/MoveTSCC.java new file mode 100644 index 00000000..ac2d08d4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/MoveTSCC.java @@ -0,0 +1,207 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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 io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Move Trunking System Control Channel (TSCC) + */ +public class MoveTSCC extends CSBKMessage +{ + private static final int[] RESERVED = new int[]{16, 17, 18, 19, 20, 20, 22, 23, 24}; + private static final int[] MASK = new int[]{25, 26, 27, 28, 29}; + private static final int[] RESERVED_2 = new int[]{30, 31, 32, 33, 34}; + private static final int REGISTRATION_REQUIRED_FLAG = 35; + private static final int[] BACKOFF = new int[]{36, 37, 38, 39}; + private static final int[] RESERVED_3 = new int[]{40, 41, 42, 43}; + private static final int[] CHANNEL_NUMBER = new int[]{44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private RadioIdentifier mRadioIdentifier; + private DMRChannel mDMRChannel; + private AbsoluteChannelParameters mAbsoluteChannelParameters; + private List mIdentifiers; + + /** + * Constructs a single-block CSBK 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 MoveTSCC(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public MoveTSCC(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" MOVE TRUNK CONTROL CHANNEL ").append(getChannel()); + + if(hasRadioIdentifier()) + { + sb.append(" TO:").append(getRadioIdentifier()); + } + + return sb.toString(); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + public DMRChannel getChannel() + { + if(mDMRChannel == null) + { + if(hasAbsoluteChannelParameters()) + { + mDMRChannel = getAbsoluteChannelParameters().getChannel(); + } + else + { + mDMRChannel = new DMRLogicalChannel(getMessage().getInt(CHANNEL_NUMBER), 1); + } + } + + return mDMRChannel; + } + + /** + * Mobile subscriber ID masking value. See: 102 361-4 p6.1.3 + */ + public int getMask() + { + return getMessage().getInt(MASK); + } + + /** + * Acknowledged radio identifier + */ + public RadioIdentifier getRadioIdentifier() + { + if(mRadioIdentifier == null) + { + mRadioIdentifier = DMRRadio.createTo(getMessage().getInt(RADIO)); + } + + return mRadioIdentifier; + } + + public boolean hasRadioIdentifier() + { + return getMessage().getInt(RADIO) != 0; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasRadioIdentifier()) + { + mIdentifiers.add(getRadioIdentifier()); + } + mIdentifiers.add(getChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Preamble.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Preamble.java new file mode 100644 index 00000000..808c8d21 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Preamble.java @@ -0,0 +1,161 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard; + +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.module.decode.dmr.DMRSyncPattern; +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.List; + +/** + * Preamble + */ +public class Preamble extends CSBKMessage +{ + private static final int DATA_CSBK_FLAG = 16; + private static final int RADIO_TALKGROUP_FLAG = 17; + private static final int[] BLOCKS_TO_FOLLOW = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mTargetAddress; + private RadioIdentifier mSourceAddress; + private List 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 Preamble(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()); + sb.append(" CSBK PREAMBLE FM:").append(getSourceAddress()); + sb.append(" TO:").append(getTargetAddress()); + sb.append(isCSBKPreamble() ? " CSBK" : " DATA"); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + public boolean isTalkgroupTargetAddress() + { + return getMessage().get(RADIO_TALKGROUP_FLAG); + } + + /** + * Indicates if this is a preamble for a data packet or sequence + */ + public boolean isDataPreamble() + { + return getMessage().get(DATA_CSBK_FLAG); + } + + /** + * Indicates if this is a preamble for CSBK content + */ + public boolean isCSBKPreamble() + { + return !getMessage().get(DATA_CSBK_FLAG); + } + + /** + * Number of data blocks that will follow this preamble + */ + public int getBlocksToFollow() + { + return getMessage().getInt(BLOCKS_TO_FOLLOW); + } + + /** + * Target radio address that is either a radio or talkgroup identifier + */ + public IntegerIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + if(isTalkgroupTargetAddress()) + { + mTargetAddress = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + else + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + } + + return mTargetAddress; + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceAddress() + { + if(mSourceAddress == null) + { + mSourceAddress = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceAddress; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceAddress()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Protect.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Protect.java new file mode 100644 index 00000000..985aa6b5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Protect.java @@ -0,0 +1,145 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard; + +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.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.type.ProtectKind; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Protect + */ +public class Protect extends CSBKMessage +{ + private static final int[] PROTECT_KIND = new int[]{28, 29, 30}; + private static final int TALKGROUP_FLAG = 31; + protected static final int[] DESTINATION = 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}; + protected static final int[] SOURCE = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private IntegerIdentifier mDestinationId; + + /** + * Constructs a single-block CSBK 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 Protect(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" PROTECT: ").append(getProtectKind()); + sb.append(" FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationId()); + return sb.toString(); + } + + /** + * Kind or type of protect command being issued. + */ + public ProtectKind getProtectKind() + { + return ProtectKind.fromValue(getMessage().getInt(PROTECT_KIND)); + } + + /** + * Source radio ID + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination ID + */ + public IntegerIdentifier getDestinationId() + { + if(mDestinationId == null) + { + if(getMessage().get(TALKGROUP_FLAG)) + { + mDestinationId = DMRTalkgroup.create(getMessage().getInt(DESTINATION)); + } + else + { + mDestinationId = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + } + + return mDestinationId; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationId()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/Acknowledge.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/Acknowledge.java new file mode 100644 index 00000000..a944078f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/Acknowledge.java @@ -0,0 +1,184 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge; + +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.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.type.AcknowledgeType; +import io.github.dsheirer.module.decode.dmr.message.type.Reason; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Acknowledge Response + */ +public class Acknowledge extends CSBKMessage +{ + private static final int TARGET_GROUP_INDIVIDUAL_FLAG = 16; + protected static final int[] RESPONSE_INFO = new int[]{16, 17, 18, 19, 20, 21, 22}; + private static final int[] ACKNOWLEDGE_TYPE = new int[]{23, 24}; + //Note: acknowledge type is the first 2 bits of the reason code - intentional overlap + private static final int[] REASON_CODE = new int[]{23, 24, 25, 26, 27, 28, 29, 30}; + private static final int RESERVED = 31; + private static final int[] TARGET_ADDRESS = 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[] ADDITIONAL_INFO_SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + protected List mIdentifiers; + private IntegerIdentifier mTargetAddress; + private RadioIdentifier mSourceRadio; + + /** + * 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 Acknowledge(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()); + sb.append(" ").append(getAcknowledgeType()); + sb.append(" REASON:").append(getReason()); + sb.append(" FM:").append(getSourceRadio()); + sb.append(" TO:").append(getTargetAddress()); + + return sb.toString(); + } + + /** + * Indicates if the target address is a talkgroup (true) or radio/gateway (false). + */ + public boolean isTargetAddressTalkgroup() + { + return getMessage().get(TARGET_GROUP_INDIVIDUAL_FLAG); + } + + /** + * Acknowledgement message type + */ + public AcknowledgeType getAcknowledgeType() + { + return AcknowledgeType.fromValue(getMessage().getInt(ACKNOWLEDGE_TYPE)); + } + + /** + * Reason code. + * + * Note: the reason entries are coupled with the acknowledgement type and can optionally indicate when the + * additional information field contains amplifying information. + */ + public Reason getReason() + { + return getReason(getMessage()); + } + + /** + * Utility method to determine the reason code for the specified message + * @param message containing a reason code field + * @return reason + */ + public static Reason getReason(CorrectedBinaryMessage message) + { + return Reason.fromValue(message.getInt(REASON_CODE)); + } + + public IntegerIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + if(isTargetAddressTalkgroup()) + { + mTargetAddress = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + else + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + } + + return mTargetAddress; + } + + public boolean hasSourceRadio() + { + return true; //TODO: delineate valid reason codes here + } + + /** + * Source radio identifier + * + * Note: the source radio field is dual-purpose and contains either a source radio address or + * additional information, depending on the reason code and/or acknowledgement type. Use the + * hasSourceRadio() method to first check if this message carries a source radio identifier. + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(ADDITIONAL_INFO_SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + + if(hasSourceRadio()) + { + mIdentifiers.add(getSourceRadio()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/AcknowledgeStatus.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/AcknowledgeStatus.java new file mode 100644 index 00000000..8ac0c323 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/AcknowledgeStatus.java @@ -0,0 +1,90 @@ +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRUnitStatus; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Mobile Subscriber Status Acknowledged + * + * Indicates the controller accepts the status message and will forward to the status service. + */ +public class AcknowledgeStatus extends Acknowledge +{ + private DMRUnitStatus mUnitStatus; + + /** + * 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 AcknowledgeStatus(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Status value sent by the mobile subscriber + */ + public DMRUnitStatus getUnitStatus() + { + if(mUnitStatus == null) + { + mUnitStatus = DMRUnitStatus.create(getMessage().getInt(RESPONSE_INFO)); + } + + return mUnitStatus; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" ACKNOWLEDGE STATUS:").append(getUnitStatus()); + sb.append(" TO:").append(getTargetAddress()); + + if(hasSourceRadio()) + { + sb.append(" FM:").append(getSourceRadio()); + } + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + + if(hasSourceRadio()) + { + mIdentifiers.add(getSourceRadio()); + } + + mIdentifiers.add(getUnitStatus()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/RegistrationAccepted.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/RegistrationAccepted.java new file mode 100644 index 00000000..6ebe196a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/acknowledge/RegistrationAccepted.java @@ -0,0 +1,66 @@ +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Registration Accepted + */ +public class RegistrationAccepted extends Acknowledge +{ + /** + * 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 RegistrationAccepted(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Power Save offset that the mobile subscriber should monitor when in power save mode. + * @return frame, 1-8 + */ + public int getPowerSaveOffset() + { + return getMessage().getInt(RESPONSE_INFO); + } + + /** + * Indicates if a power save offset value is specified by the TSCC + */ + public boolean hasPowerSaveOffset() + { + return getPowerSaveOffset() > 0; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" REGISTRATION ACCEPTED TO:").append(getTargetAddress()); + sb.append(" FM:").append(getSourceRadio()); + + if(hasPowerSaveOffset()) + { + sb.append(" POWER SAVE OFFSET:").append(getPowerSaveOffset()); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/Ahoy.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/Ahoy.java new file mode 100644 index 00000000..ea09ec2e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/Ahoy.java @@ -0,0 +1,135 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.type.ServiceKind; + +/** + * DMR Tier III - Ahoy! + */ +public abstract class Ahoy extends CSBKMessage +{ + private static final int[] SERVICE_OPTIONS_MIRROR = new int[]{16, 17, 18, 19, 20, 21, 22}; + private static final int ENCRYPTED_SERVICE_OPTION_FLAG = 17; + protected static final int SERVICE_KIND_FLAG = 23; + private static final int AMBIENT_LISTENING_SERVICE_FLAG = 24; + private static final int TALKGROUP_FLAG = 25; + protected static final int[] APPENDED_BLOCKS_OR_STATUS = new int[]{26, 27}; + private static final int[] SERVICE_KIND = new int[]{28, 29, 30, 31}; + protected static final int[] TARGET_ADDRESS = 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}; + protected static final int[] MULTI_PURPOSE_FIELD = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mTargetAddress; + + /** + * 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 Ahoy(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Utility method to lookup service kind for an AHOY message + */ + public static ServiceKind getServiceKind(CorrectedBinaryMessage message) + { + return ServiceKind.fromValue(message.getInt(SERVICE_KIND)); + } + + /** + * Value of the multi-purpose field. + */ + public int getMultiPurposeFieldValue() + { + return getMessage().getInt(MULTI_PURPOSE_FIELD); + } + + /** + * Service Kind for this message + */ + public ServiceKind getServiceKind() + { + return getServiceKind(getMessage()); + } + + /** + * Flag that has meaning depending on the Service Kind value + */ + public boolean isServiceKindFlag() + { + return getMessage().get(SERVICE_KIND_FLAG); + } + + /** + * Indicates if the calling party requests Ambient Listening Service (ALS). + * @return + */ + public boolean isAmbientListeningServiceRequest() + { + return getMessage().get(AMBIENT_LISTENING_SERVICE_FLAG); + } + + /** + * Target Talkgroup or Radio Identifier + */ + public IntegerIdentifier getTargetAddress() + { + if(mTargetAddress == null) + { + if(isTalkgroupTarget()) + { + mTargetAddress = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + else + { + mTargetAddress = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + } + + return mTargetAddress; + } + + /** + * Indicates if the target address is a talkgroup (true) or radio (false). + */ + public boolean isTalkgroupTarget() + { + return getMessage().get(TALKGROUP_FLAG); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/AuthenticateRegisterRadioCheck.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/AuthenticateRegisterRadioCheck.java new file mode 100644 index 00000000..e8482fb4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/AuthenticateRegisterRadioCheck.java @@ -0,0 +1,159 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.type.Gateway; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III Ahoy - Authentication Challenge + */ +public class AuthenticateRegisterRadioCheck extends Ahoy +{ + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + + /** + * 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 AuthenticateRegisterRadioCheck(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" ").append(getCommand()).append(":").append(getTargetAddress()); + + if(isAuthenticateCommand()) + { + sb.append(" CHALLENGE VALUE:").append(getChallengeValue()); + } + + return sb.toString(); + } + + /** + * Gateway address from the multi-purpose field + */ + public Gateway getGateway() + { + return Gateway.fromValue(getMultiPurposeFieldValue()); + } + + /** + * Command for this message + */ + public String getCommand() + { + switch(getGateway()) + { + case TSI: + return "RADIO CHECK"; + default: + return "AUTHENTICATE"; + } + } + + /** + * Indicates if this is an authentication command indicating that there is a challenge value. + */ + public boolean isAuthenticateCommand() + { + return getGateway() != Gateway.TSI; + } + + /** + * Challenge value created by the base station to which the target address must authenticate. + */ + public String getChallengeValue() + { + return String.format("%06X", getMessage().getInt(MULTI_PURPOSE_FIELD)); + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMultiPurposeFieldValue()); + } + + return mSourceRadio; + } + + /** + * Indicates if this command includes a source radio (ie gateway) identifier + */ + public boolean hasSourceRadio() + { + return !isAuthenticateCommand(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + + if(hasSourceRadio()) + { + mIdentifiers.add(getSourceRadio()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/CancelCall.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/CancelCall.java new file mode 100644 index 00000000..1671394d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/CancelCall.java @@ -0,0 +1,104 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III Ahoy - Cancel Call + */ +public class CancelCall extends Ahoy +{ + private RadioIdentifier mSourceRadio; + private List 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 CancelCall(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" CANCEL CALL TO:").append(getTargetAddress()); + sb.append(" FM:").append(getSourceRadio()); + + return sb.toString(); + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMultiPurposeFieldValue()); + } + + return mSourceRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/ServiceRadioCheck.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/ServiceRadioCheck.java new file mode 100644 index 00000000..2572b7cd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/ServiceRadioCheck.java @@ -0,0 +1,129 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III Ahoy - Voice/Data Service Radio Check for Individual/Talkgroup + */ +public class ServiceRadioCheck extends Ahoy +{ + private RadioIdentifier mSourceRadio; + private List 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 ServiceRadioCheck(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" ").append(getServiceDescription()); + sb.append(" SERVICE RADIO CHECK (").append(isTalkgroupTarget() ? "TALKGROUP" : "INDIVIDUAL"); + sb.append(") TO:").append(getTargetAddress()); + sb.append(" FM:").append(getSourceRadio()); + + return sb.toString(); + } + + /** + * Description of the service type that is being checked + */ + public String getServiceDescription() + { + switch(getServiceKind()) + { + case FULL_DUPLEX_MS_TO_MS_PACKET_CALL_SERVICE: + case INDIVIDUAL_PACKET_CALL_SERVICE: + case TALKGROUP_PACKET_CALL_SERVICE: + return "PACKET"; + case INDIVIDUAL_UDT_SHORT_DATA_CALL_SERVICE: + case TALKGROUP_UDT_SHORT_DATA_CALL_SERVICE: + return "SHORT DATA"; + case FULL_DUPLEX_MS_TO_MS_VOICE_CALL_SERVICE: + case INDIVIDUAL_VOICE_CALL_SERVICE: + case TALKGROUP_VOICE_CALL_SERVICE: + return "VOICE"; + } + + return "UNKNOWN"; + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMultiPurposeFieldValue()); + } + + return mSourceRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/StunReviveKill.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/StunReviveKill.java new file mode 100644 index 00000000..9e1974e2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/StunReviveKill.java @@ -0,0 +1,152 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.type.Gateway; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III Ahoy - Stun, Revive or Kill a Mobile Radio + */ +public class StunReviveKill extends Ahoy +{ + private RadioIdentifier mSourceRadio; + private List 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 StunReviveKill(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" ").append(getCommand()).append(" RADIO:").append(getTargetAddress()); + sb.append(" FM:").append(getSourceRadio()); + + return sb.toString(); + } + + /** + * Gateway address from the multi-purpose field + */ + public Gateway getGateway() + { + return Gateway.fromValue(getMultiPurposeFieldValue()); + } + + /** + * Command for this message + */ + public String getCommand() + { + switch(getGateway()) + { + case KILLI: + return "KILL"; + case STUNI: + if(isStunCommand()) + { + return "STUN"; + } + else + { + return "REVIVE"; + } + default: + return "UNKNOWN"; + } + } + + /** + * Indicates if this is a Stun command + */ + public boolean isStunCommand() + { + return !getMessage().get(SERVICE_KIND_FLAG); + } + + /** + * Indicates if this is a Revive command + */ + public boolean isReviveCommand() + { + return !getMessage().get(SERVICE_KIND_FLAG); + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMultiPurposeFieldValue()); + } + + return mSourceRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/UnknownAhoy.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/UnknownAhoy.java new file mode 100644 index 00000000..fbbc9537 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/ahoy/UnknownAhoy.java @@ -0,0 +1,153 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.type.Gateway; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III Ahoy - Unknown Ahoy Type + */ +public class UnknownAhoy extends Ahoy +{ + private RadioIdentifier mSourceRadio; + private List 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 UnknownAhoy(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(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" AHOY ").append(getServiceKind()); + sb.append(" TO:").append(getTargetAddress()); + sb.append(" FM:").append(getSourceRadio()); + + return sb.toString(); + } + + /** + * Gateway address from the multi-purpose field + */ + public Gateway getGateway() + { + return Gateway.fromValue(getMultiPurposeFieldValue()); + } + + /** + * Command for this message + */ + public String getCommand() + { + switch(getGateway()) + { + case KILLI: + return "KILL"; + case STUNI: + if(isStunCommand()) + { + return "STUN"; + } + else + { + return "REVIVE"; + } + default: + return "UNKNOWN"; + } + } + + /** + * Indicates if this is a Stun command + */ + public boolean isStunCommand() + { + return !getMessage().get(SERVICE_KIND_FLAG); + } + + /** + * Indicates if this is a Revive command + */ + public boolean isReviveCommand() + { + return !getMessage().get(SERVICE_KIND_FLAG); + } + + /** + * Source radio identifier + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMultiPurposeFieldValue()); + } + + return mSourceRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetAddress()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AdjacentSiteInformation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AdjacentSiteInformation.java new file mode 100644 index 00000000..beedd447 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AdjacentSiteInformation.java @@ -0,0 +1,272 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Announcement Message - Adjacent/Neighbor Site Information + */ +public class AdjacentSiteInformation extends Announcement implements ITimeslotFrequencyReceiver +{ + private static final int NEIGHBOR_SYSTEM_IDENTITY_CODE_OFFSET = 21; + private static final int NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG = 56; + private static final int ACTIVE_NETWORK_CONNECTION_FLAG = 57; + private static final int[] CONFIRMED_CHANNEL_PRIORITY = new int[]{58, 59, 60}; + private static final int[] ADJACENT_CHANNEL_PRIORITY = new int[]{61, 62, 63}; + private static final int[] RESERVED = new int[]{64, 65, 66, 67}; + private static final int[] NEIGHBOR_CHANNEL_NUMBER = new int[]{68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private SystemIdentityCode mNeighborSystemIdentityCode; + private List mIdentifiers; + + private AbsoluteChannelParameters mAbsoluteChannelParameters; + private DMRChannel mChannel; + + /** + * Constructs a single-block CSBK 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 AdjacentSiteInformation(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC 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 AdjacentSiteInformation(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + //Timeslot hard-coded to zero for control channel + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + /** + * DMR Channel and Timeslot for neighbor site + */ + public DMRChannel getNeighborChannel() + { + if(hasAbsoluteChannelParameters()) + { + return mAbsoluteChannelParameters.getChannel(); + } + + if(mChannel == null) + { + //Timeslot hard-coded to 1 + mChannel = new DMRLogicalChannel(getMessage().getInt(NEIGHBOR_CHANNEL_NUMBER), 1); + } + + return mChannel; + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NEIGHBOR NETWORK:").append(getNeighborSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getNeighborSystemIdentityCode().getSite()); + sb.append(" ").append(getNeighborChannel()); + sb.append(" THIS NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Indicates if the active network connection status is available. + * + * See also: isActiveNetworkConnection() + */ + public boolean hasNetworkConnectionStatus() + { + return getMessage().get(NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG); + } + + /** + * Indicates if this site has an active network connection + * + * See also: hasNetworkConnectionStatus() + */ + public boolean isActiveNetworkConnection() + { + return getMessage().get(ACTIVE_NETWORK_CONNECTION_FLAG); + } + + /** + * Confirmed channel priority + */ + public int getConfirmedChannelPriority() + { + return getMessage().getInt(CONFIRMED_CHANNEL_PRIORITY); + } + + /** + * Adjacent channel priority + */ + public int getAdjacentChannelPriority() + { + return getMessage().getInt(ADJACENT_CHANNEL_PRIORITY); + } + + /** + * Control Channel Number for the neighbor site + */ + public int getNeighborChannelNumber() + { + return getMessage().getInt(NEIGHBOR_CHANNEL_NUMBER); + } + + /** + * Neighbor Site System Identity Code structure + */ + public SystemIdentityCode getNeighborSystemIdentityCode() + { + if(mNeighborSystemIdentityCode == null) + { + mNeighborSystemIdentityCode = new SystemIdentityCode(getMessage(), + NEIGHBOR_SYSTEM_IDENTITY_CODE_OFFSET, false); + } + + return mNeighborSystemIdentityCode; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } + + /** + * Logical Slot Number(s) for channels contained in this message + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + if(getNeighborChannel() instanceof DMRLogicalChannel) + { + return ((DMRLogicalChannel)getNeighborChannel()).getLSNArray(); + } + + return new int[0]; + } + + /** + * Applies the timeslot frequency lookup information to channels contained in this message + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(getNeighborChannel() instanceof DMRLogicalChannel) + { + DMRLogicalChannel channel = (DMRLogicalChannel)getNeighborChannel(); + + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + channel.setTimeslotFrequency(timeslotFrequency); + } + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceChannelFrequency.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceChannelFrequency.java new file mode 100644 index 00000000..bdbedeb1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceChannelFrequency.java @@ -0,0 +1,131 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tier III Announcement - Logical channel:frequency relationship. + */ +public class AnnounceChannelFrequency extends Announcement +{ + private AbsoluteChannelParameters mAbsoluteChannelParameters; + private List 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 AnnounceChannelFrequency(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" ANNOUNCE CHANNEL"); + if(hasAbsoluteChannelParameters()) + { + sb.append(getAbsoluteChannelParameters().getChannel()); + sb.append(" CC:").append(getAbsoluteChannelParameters().getColorCode()); + } + + return sb.toString(); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasAbsoluteChannelParameters()) + { + mIdentifiers.add(getAbsoluteChannelParameters().getChannel()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceWithdrawTSCC.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceWithdrawTSCC.java new file mode 100644 index 00000000..7c056fd1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/AnnounceWithdrawTSCC.java @@ -0,0 +1,256 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Announcement or Widthdraw TSCC + */ +public class AnnounceWithdrawTSCC extends Announcement +{ + //Broadcast Parameters 1: 21-34 + private static final int[] RESERVED = new int[]{21, 22, 23, 24}; + private static final int[] COLOR_CODE_CHAN_1 = new int[]{25, 26, 27, 28}; + private static final int[] COLOR_CODE_CHAN_2 = new int[]{29, 30, 31, 32}; + private static final int ADD_WITHDRAW_CHAN_1_FLAG = 33; + private static final int ADD_WITHDRAW_CHAN_2_FLAG = 34; + + //Broadcast Parameters 2: 56-79 + private static final int[] CHANNEL_NUMBER_1 = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67}; + private static final int[] CHANNEL_NUMBER_2 = new int[]{68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private DMRChannel mChannel1; + private DMRChannel mChannel2; + private AbsoluteChannelParameters mAbsoluteChannelParameters; + private List mIdentifiers; + + /** + * Constructs a single-block CSBK 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 AnnounceWithdrawTSCC(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public AnnounceWithdrawTSCC(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + //Control channel timeslot hard-coded to 0 + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(hasChannel1()) + { + sb.append((isChannel1Add() ? " ADD CHAN:" : " WITHDRAW CHAN:")); + sb.append(getChannel1()).append(" CC:").append(getChannel1ColorCode()); + } + + if(hasChannel2()) + { + sb.append((isChannel2Add() ? " ADD CHAN:" : " WITHDRAW CHAN:")); + sb.append(getChannel2()).append(" CC:").append(getChannel2ColorCode()); + } + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + /** + * Indicates if this message has channel 1 information + */ + public boolean hasChannel1() + { + return hasAbsoluteChannelParameters() || getMessage().getInt(CHANNEL_NUMBER_1) != 0; + } + + /** + * Indicates if this message has channel 2 information + */ + public boolean hasChannel2() + { + return getMessage().getInt(CHANNEL_NUMBER_2) != 0; + } + + /** + * Color code for channel 1 + */ + public int getChannel1ColorCode() + { + if(hasAbsoluteChannelParameters()) + { + return getAbsoluteChannelParameters().getColorCode(); + } + + return getMessage().getInt(COLOR_CODE_CHAN_1); + } + + /** + * Color code for channel 2 + */ + public int getChannel2ColorCode() + { + return getMessage().getInt(COLOR_CODE_CHAN_2); + } + + /** + * Indicates if channel 1 is being added (true) or withdrawn (false) + */ + public boolean isChannel1Add() + { + return getMessage().get(ADD_WITHDRAW_CHAN_1_FLAG); + } + + /** + * Indicates if channel 2 is being added (true) or withdrawn (false) + */ + public boolean isChannel2Add() + { + return getMessage().get(ADD_WITHDRAW_CHAN_2_FLAG); + } + + /** + * Channel 1 + */ + public DMRChannel getChannel1() + { + if(mChannel1 == null) + { + if(hasAbsoluteChannelParameters()) + { + mChannel1 = getAbsoluteChannelParameters().getChannel(); + } + else + { + mChannel1 = new DMRLogicalChannel(getMessage().getInt(CHANNEL_NUMBER_1), 1); + } + } + + return mChannel1; + } + + /** + * Channel 2 + */ + public DMRChannel getChannel2() + { + if(mChannel2 == null) + { + mChannel2 = new DMRLogicalChannel(getMessage().getInt(CHANNEL_NUMBER_2), 1); + } + + return mChannel2; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/Announcement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/Announcement.java new file mode 100644 index 00000000..0826e3bd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/Announcement.java @@ -0,0 +1,135 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Announcement Message + */ +public class Announcement extends CSBKMessage +{ + private static final int[] ANNOUNCEMENT_TYPE = new int[]{16, 17, 18, 19, 20}; + private static final int[] PARAMS_1 = new int[]{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}; + private static final int REGISTRATION_REQUIRED_FLAG = 35; + private static final int[] BACKOFF = new int[]{36, 37, 38, 39}; + private static final int SYSTEM_IDENTITY_CODE_OFFSET = 40; + private static final int[] PARAMS_2 = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private SystemIdentityCode mSystemIdentityCode; + + /** + * 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 Announcement(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()); + sb.append(" ANNOUNCEMENT ").append(getAnnouncementType()); + + return sb.toString(); + } + + /** + * Indicates if registration is required on this site + */ + public boolean isRegistrationRequired() + { + return getMessage().get(REGISTRATION_REQUIRED_FLAG); + } + + /** + * Backoff value + */ + public int getBackoff() + { + return getMessage().getInt(BACKOFF); + } + + /** + * System Identity Code structure + */ + public SystemIdentityCode getSystemIdentityCode() + { + if(mSystemIdentityCode == null) + { + mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, true); + } + + return mSystemIdentityCode; + } + /** + * Announcement message type for this message + */ + public AnnouncementType getAnnouncementType() + { + return getAnnouncementType(getMessage()); + } + + /** + * Utility to get announcement message type for the specified message + */ + public static AnnouncementType getAnnouncementType(CorrectedBinaryMessage message) + { + return AnnouncementType.fromValue(message.getInt(ANNOUNCEMENT_TYPE)); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/CallTimerParameters.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/CallTimerParameters.java new file mode 100644 index 00000000..979228b1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/CallTimerParameters.java @@ -0,0 +1,273 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Call Timer Parameters + */ +public class CallTimerParameters extends Announcement +{ + //Broadcast Parameters 1: 21-34 + private static final int[] EMERGENCY_TIMER = new int[]{21, 22, 23, 24, 25, 26, 27, 28, 29}; + private static final int[] PACKET_TIMER = new int[]{30, 31, 32, 33, 34}; + + //Broadcast Parameters 2: 56-79 + private static final int[] MOBILE_TO_MOBILE_TIMER = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67}; + private static final int[] MOBILE_TO_LINE_TIMER = new int[]{68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private List 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 CallTimerParameters(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()); + + sb.append(" CALL TIMERS EMERG:").append(getEmergencyTimerDecoded()); + sb.append(" PACKET:").append(getPacketTimerDecoded()); + sb.append(" MS-MS:").append(getMobileToMobileTimerDecoded()); + sb.append(" MS-LINE:").append(getMobileToLineTimerDecoded()); + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Emergency Timer + * @return 0 = Internal, 1-510 = value, 511 = infinity + */ + public int getEmergencyTimer() + { + return getMessage().getInt(EMERGENCY_TIMER); + } + + /** + * Decodes the numeric timer value for emergency calls + */ + public String getEmergencyTimerDecoded() + { + int timer = getEmergencyTimer(); + + if(timer == 0) + { + return "INTERNAL"; + } + else if(1 <= timer && timer <= 10) + { + return timer + " SECS"; + } + else if(11 <= timer && timer <= 20) + { + return ((timer - 8) * 5) + " SECS"; + } + else if(21 <= timer && timer <= 28) + { + return ((timer - 16) * 15) + " SECS"; + } + else if(29 <= timer && timer <= 40) + { + return ((timer - 22) * .5) + " MINS"; + } + else if(41 <= timer && timer <= 51) + { + return (timer - 31) + " MINS"; + } + else if(52 <= timer && timer <= 510) + { + return ((timer - 47) * 5) + " MINS"; + } + + return "INFINITY"; + } + + /** + * Packet Timer + * @return 0 = Internal, 1-30 = value, 31 = infinity + */ + public int getPacketTimer() + { + return getMessage().getInt(PACKET_TIMER); + } + + /** + * Decodes the numeric timer value for packet calls + */ + public String getPacketTimerDecoded() + { + int timer = getPacketTimer(); + + if(timer == 0) + { + return "INTERNAL"; + } + else if(1 <= timer && timer <= 5) + { + return timer + " SECS"; + } + else if(6 <= timer && timer <= 10) + { + return ((timer - 4) * 5) + " SECS"; + } + else if(11 <= timer && timer <= 12) + { + return ((timer - 8) * 15) + " SECS"; + } + else if(13 <= timer && timer <= 20) + { + return ((timer - 10) * .5) + " MINS"; + } + else if(21 <= timer && timer <= 25) + { + return (timer - 15) + " MINS"; + } + else if(26 <= timer && timer <= 30) + { + return ((timer - 23) * 5) + " MINS"; + } + + return "INFINITY"; + } + + /** + * Mobile-to-Mobile Call Timer + * @return 0 = Internal, 1-4094 = value, 4095 = infinity + */ + public int getMobileToMobileTimer() + { + return getMessage().getInt(MOBILE_TO_MOBILE_TIMER); + } + + /** + * Decodes the numeric timer value for mobile-2-mobile calls + */ + public String getMobileToMobileTimerDecoded() + { + int timer = getMobileToMobileTimer(); + + if(timer == 0) + { + return "INTERNAL"; + } + else if(1 <= timer && timer <= 59) + { + return timer + " SECS"; + } + else if(60 <= timer && timer <= 107) + { + return ((timer - 48) * 5) + " SECS"; + } + else if(108 <= timer && timer <= 138) + { + return ((timer - 98) * .5) + " MINS"; + } + else if(139 <= timer && timer <= 4094) + { + return (timer - 118) + " MINS"; + } + + return "INFINITY"; + } + + /** + * Mobile-to-Line Call Timer + * @return 0 = Internal, 1-510 = value, 511 = infinity + */ + public int getMobileToLineTimer() + { + return getMessage().getInt(MOBILE_TO_LINE_TIMER); + } + + /** + * Decodes the numeric timer value for mobile-2-line calls + */ + public String getMobileToLineTimerDecoded() + { + int timer = getMobileToLineTimer(); + + if(timer == 0) + { + return "INTERNAL"; + } + else if(1 <= timer && timer <= 59) + { + return timer + " SECS"; + } + else if(60 <= timer && timer <= 107) + { + return ((timer - 48) * 5) + " SECS"; + } + else if(108 <= timer && timer <= 138) + { + return ((timer - 98) * .5) + " MINS"; + } + else if(139 <= timer && timer <= 4094) + { + return (timer - 118) + " MINS"; + } + + return "INFINITY"; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/LocalTime.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/LocalTime.java new file mode 100644 index 00000000..4f6d95ae --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/LocalTime.java @@ -0,0 +1,122 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; + +/** + * DMR Tier III - Broadcast Local Time + */ +public class LocalTime extends Announcement +{ + //Broadcast Parameters 1: 21-34 + private static final int[] DAY = new int[]{21, 22, 23, 24, 25}; + private static final int[] MONTH = new int[]{26, 27, 28, 39}; + private static final int[] UTC = new int[]{30, 31, 32, 33, 34}; + + //Broadcast Parameters 2: 56-79 + private static final int[] HOUR = new int[]{56, 57, 58, 59, 60}; + private static final int[] MINUTE = new int[]{61, 62, 63, 64, 65, 66}; + private static final int[] SECOND = new int[]{67, 68, 69, 70, 71, 72}; + private static final int[] DAY_OF_WEEK = new int[]{73, 74, 75}; + private static final int[] UTC_MINUTES_OFFSET = new int[]{76, 77}; + private static final int[] RESERVED = new int[]{78, 79}; + + private List 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 LocalTime(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()); + sb.append(" LOCAL TIME:").append(new Date(getLocalTime()).toString()); + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Local time broadcast + * @return + */ + public long getLocalTime() + { + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTimeInMillis(System.currentTimeMillis()); + int year = calendar.get(Calendar.YEAR); + int month = getMessage().getInt(MONTH); + int day = getMessage().getInt(DAY); + int utcHoursOffset = getMessage().getInt(UTC); + int hour = (getMessage().getInt(HOUR) + (utcHoursOffset == 31 ? 0 : utcHoursOffset)) % 24; + int minute = getMessage().getInt(MINUTE); + int second = getMessage().getInt(SECOND); + calendar.clear(); + calendar.set(year, month, day, hour, minute, second); + return calendar.getTimeInMillis(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/MassRegistration.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/MassRegistration.java new file mode 100644 index 00000000..d82155a5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/MassRegistration.java @@ -0,0 +1,127 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Broadcast Mass Registration + */ +public class MassRegistration extends Announcement +{ + //Broadcast Parameters 1: 21-34 + private static final int[] RESERVED = new int[]{21, 22, 23, 24, 25}; + private static final int[] REGISTRATION_WINDOW = new int[]{26, 27, 28, 39}; + private static final int[] ALOHA_MASK = new int[]{30, 31, 32, 33, 34}; + + //Broadcast Parameters 2: 56-79 + private static final int[] DESTINATION_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private RadioIdentifier mDestinationRadio; + + /** + * 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 MassRegistration(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()); + sb.append(" MASS REGISTRATION"); + + if(hasDestinationRadio()) + { + sb.append(" TO:").append(getDestinationRadio()); + } + + sb.append(" ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Indicates if this message is addressing a specific radio + */ + public boolean hasDestinationRadio() + { + return getMessage().getInt(DESTINATION_RADIO) != 0; + } + + /** + * Addressed destination radio identifier + */ + public RadioIdentifier getDestinationRadio() + { + if(mDestinationRadio == null) + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION_RADIO)); + } + + return mDestinationRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasDestinationRadio()) + { + mIdentifiers.add(getDestinationRadio()); + } + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/VoteNowAdvice.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/VoteNowAdvice.java new file mode 100644 index 00000000..29fd76c7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/announcement/VoteNowAdvice.java @@ -0,0 +1,272 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Vote Now Advice + */ +public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyReceiver +{ + //Broadcast Parameters 1: 21-34 + private static final int VOTED_SYSTEM_IDENTITY_CODE_OFFSET = 21; + + //Broadcast Parameters 2: 56-79 + private static final int NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG = 56; + private static final int ACTIVE_NETWORK_CONNECTION_FLAG = 57; + private static final int[] CONFIRMED_CHANNEL_PRIORITY = new int[]{58, 59, 60}; + private static final int[] ADJACENT_CHANNEL_PRIORITY = new int[]{61, 62, 63}; + private static final int[] RESERVED = new int[]{64, 65, 66, 67}; + private static final int[] VOTED_CHANNEL_NUMBER = new int[]{68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private SystemIdentityCode mVotedSystemIdentityCode; + private List mIdentifiers; + private DMRChannel mChannel; + private AbsoluteChannelParameters mAbsoluteChannelParameters; + + /** + * Constructs a single-block CSBK 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 VoteNowAdvice(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public VoteNowAdvice(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" VOTED NETWORK:").append(getVotedSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getVotedSystemIdentityCode().getSite()); + sb.append(" CHAN:").append(getChannel()); + sb.append(" THIS ").append(getSystemIdentityCode().getModel()); + sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + + return sb.toString(); + } + + /** + * Indicates if the active network connection status is available. + * + * See also: isActiveNetworkConnection() + */ + public boolean hasNetworkConnectionStatus() + { + return getMessage().get(NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG); + } + + /** + * Indicates if this site has an active network connection + * + * See also: hasNetworkConnectionStatus() + */ + public boolean isActiveNetworkConnection() + { + return getMessage().get(ACTIVE_NETWORK_CONNECTION_FLAG); + } + + /** + * Confirmed channel priority + */ + public int getConfirmedChannelPriority() + { + return getMessage().getInt(CONFIRMED_CHANNEL_PRIORITY); + } + + /** + * Adjacent channel priority + */ + public int getAdjacentChannelPriority() + { + return getMessage().getInt(ADJACENT_CHANNEL_PRIORITY); + } + + /** + * Control Channel Number for the voted site + */ + public int getVotedChannelNumber() + { + return getMessage().getInt(VOTED_CHANNEL_NUMBER); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + /** + * DMR Channel and Timeslot for the channel grant + */ + public DMRChannel getChannel() + { + if(hasAbsoluteChannelParameters()) + { + return mAbsoluteChannelParameters.getChannel(); + } + + if(mChannel == null) + { + mChannel = new DMRLogicalChannel(getMessage().getInt(VOTED_CHANNEL_NUMBER), 1); + } + + return mChannel; + } + + /** + * Voted Site System Identity Code structure + */ + public SystemIdentityCode getVotedSystemIdentityCode() + { + if(mVotedSystemIdentityCode == null) + { + mVotedSystemIdentityCode = new SystemIdentityCode(getMessage(), VOTED_SYSTEM_IDENTITY_CODE_OFFSET, false); + } + + return mVotedSystemIdentityCode; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } + + /** + * Logical Slot Number(s) for channels contained in this message + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + if(getChannel() instanceof DMRLogicalChannel) + { + return ((DMRLogicalChannel)getChannel()).getLSNArray(); + } + + return new int[0]; + } + + /** + * Applies the timeslot frequency lookup information to channels contained in this message + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(getChannel() instanceof DMRLogicalChannel) + { + DMRLogicalChannel channel = (DMRLogicalChannel)getChannel(); + + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + channel.setTimeslotFrequency(timeslotFrequency); + } + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/BroadcastTalkgroupVoiceChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/BroadcastTalkgroupVoiceChannelGrant.java new file mode 100644 index 00000000..9982e24f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/BroadcastTalkgroupVoiceChannelGrant.java @@ -0,0 +1,155 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +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.DMRSyncPattern; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Channel Grant - Voice - Broadcast Talkgroup + */ +public class BroadcastTalkgroupVoiceChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private TalkgroupIdentifier mDestinationTalkgroup; + + /** + * Constructs a single-block CSBK 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 BroadcastTalkgroupVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public BroadcastTalkgroupVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" BROADCAST TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationTalkgroup()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public TalkgroupIdentifier getDestinationTalkgroup() + { + if(mDestinationTalkgroup == null) + { + mDestinationTalkgroup = DMRTalkgroup.create(getMessage().getInt(DESTINATION)); + } + + return mDestinationTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationTalkgroup()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/ChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/ChannelGrant.java new file mode 100644 index 00000000..e01d303f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/ChannelGrant.java @@ -0,0 +1,191 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; +import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; +import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; +import io.github.dsheirer.module.decode.dmr.message.type.DataType; + +import java.util.List; + +/** + * DMR Tier III - Channel Grant + */ +public abstract class ChannelGrant 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[] TIMESLOT = new int[]{28}; + protected static final int[] DESTINATION = 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}; + protected static final int[] SOURCE = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79}; + + private DMRChannel mChannel; + private AbsoluteChannelParameters mAbsoluteChannelParameters; + + /** + * Constructs a single-block CSBK 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 ChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock continuation containing absolute channel parameters + */ + public ChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + this(syncPattern, message, cach, slotType, timestamp, timeslot); + + if(multiBlock != null) + { + mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, + getChannelGrantTimeslot()); + } + } + + /** + * Checks CRC and sets the message valid flag according to the results. + */ + public void checkCRC() + { + if(getSlotType().getDataType() == DataType.MBC_HEADER) + { + checkMultiBlockCRC(getAbsoluteChannelParameters()); + } + else + { + //Use the standard CSBK CRC check + super.checkCRC(); + } + } + + /** + * Timeslot to use for the channel grant + */ + private int getChannelGrantTimeslot() + { + return getMessage().getInt(TIMESLOT) + 1; + } + + /** + * Channel number for the channel grant + */ + private int getChannelNumber() + { + return getMessage().getInt(CHANNEL_NUMBER); + } + + /** + * Absolute channel parameters structure + */ + public AbsoluteChannelParameters getAbsoluteChannelParameters() + { + return mAbsoluteChannelParameters; + } + + /** + * Indicates if this message has an absolute channel parameters structure + */ + public boolean hasAbsoluteChannelParameters() + { + return getAbsoluteChannelParameters() != null; + } + + /** + * DMR Channel and Timeslot for the channel grant + */ + public DMRChannel getChannel() + { + if(hasAbsoluteChannelParameters()) + { + return mAbsoluteChannelParameters.getChannel(); + } + + if(mChannel == null) + { + mChannel = new DMRLogicalChannel(getChannelNumber(), getChannelGrantTimeslot()); + } + + return mChannel; + } + + /** + * Logical Slot Number(s) for channels contained in this message + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + if(getChannel() instanceof DMRLogicalChannel) + { + return ((DMRLogicalChannel)getChannel()).getLSNArray(); + } + + return new int[0]; + } + + /** + * Applies the timeslot frequency lookup information to channels contained in this message + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(getChannel() instanceof DMRLogicalChannel) + { + DMRLogicalChannel channel = (DMRLogicalChannel)getChannel(); + + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + channel.setTimeslotFrequency(timeslotFrequency); + } + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateDataChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateDataChannelGrant.java new file mode 100644 index 00000000..dfef0d4b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateDataChannelGrant.java @@ -0,0 +1,168 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Duplex Channel Grant - Data - Private/Individual + */ +public class DuplexPrivateDataChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + private static final int TARGET_IS_SOURCE_ADDRESS_FLAG = 31; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mDestinationRadio; + + /** + * Constructs a single-block CSBK 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 DuplexPrivateDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public DuplexPrivateDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" DUPLEX PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationRadio()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + if(getMessage().get(TARGET_IS_SOURCE_ADDRESS_FLAG)) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(DESTINATION)); + } + else + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public RadioIdentifier getDestinationRadio() + { + if(mDestinationRadio == null) + { + if(getMessage().get(TARGET_IS_SOURCE_ADDRESS_FLAG)) + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(SOURCE)); + } + else + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + } + + return mDestinationRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateVoiceChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateVoiceChannelGrant.java new file mode 100644 index 00000000..e75a4d1e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/DuplexPrivateVoiceChannelGrant.java @@ -0,0 +1,168 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Duplex Channel Grant - Voice - Private/Individual + */ +public class DuplexPrivateVoiceChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + private static final int TARGET_IS_SOURCE_ADDRESS_FLAG = 31; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mDestinationRadio; + + /** + * Constructs a single-block CSBK 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 DuplexPrivateVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public DuplexPrivateVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" DUPLEX PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationRadio()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + if(getMessage().get(TARGET_IS_SOURCE_ADDRESS_FLAG)) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(DESTINATION)); + } + else + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public RadioIdentifier getDestinationRadio() + { + if(mDestinationRadio == null) + { + if(getMessage().get(TARGET_IS_SOURCE_ADDRESS_FLAG)) + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(SOURCE)); + } + else + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + } + + return mDestinationRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateDataChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateDataChannelGrant.java new file mode 100644 index 00000000..d204a4dc --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateDataChannelGrant.java @@ -0,0 +1,167 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Channel Grant - Data - Private/Individual + */ +public class PrivateDataChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + private static final int HI_RATE_PACKET_CHANNEL_FLAG = 31; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mDestinationRadio; + + /** + * Constructs a single-block CSBK 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 PrivateDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public PrivateDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + if(isHighRatePacketChannel()) + { + sb.append(" DUAL-SLOT HI-RATE "); + } + + sb.append(" PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationRadio()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the channel grant is for single-slot data (false) or dual-slot data (true) + */ + public boolean isHighRatePacketChannel() + { + return getMessage().get(HI_RATE_PACKET_CHANNEL_FLAG); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public RadioIdentifier getDestinationRadio() + { + if(mDestinationRadio == null) + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + + return mDestinationRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateVoiceChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateVoiceChannelGrant.java new file mode 100644 index 00000000..14d6bcf5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/PrivateVoiceChannelGrant.java @@ -0,0 +1,153 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Channel Grant - Voice - Private/Individual + */ +public class PrivateVoiceChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private RadioIdentifier mDestinationRadio; + + /** + * Constructs a single-block CSBK 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 PrivateVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public PrivateVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationRadio()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public RadioIdentifier getDestinationRadio() + { + if(mDestinationRadio == null) + { + mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION)); + } + + return mDestinationRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupDataChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupDataChannelGrant.java new file mode 100644 index 00000000..9355dd42 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupDataChannelGrant.java @@ -0,0 +1,169 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +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.DMRSyncPattern; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Channel Grant - Data - Talkgroup + */ +public class TalkgroupDataChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + private static final int HI_RATE_PACKET_CHANNEL_FLAG = 31; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private TalkgroupIdentifier mDestinationTalkgroup; + + /** + * Constructs a single-block CSBK 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 TalkgroupDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public TalkgroupDataChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + if(isHighRatePacketChannel()) + { + sb.append(" DUAL-SLOT HI-RATE "); + } + + sb.append(" TALKGROUP DATA CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationTalkgroup()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the channel grant is for single-slot data (false) or dual-slot data (true) + */ + public boolean isHighRatePacketChannel() + { + return getMessage().get(HI_RATE_PACKET_CHANNEL_FLAG); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public TalkgroupIdentifier getDestinationTalkgroup() + { + if(mDestinationTalkgroup == null) + { + mDestinationTalkgroup = DMRTalkgroup.create(getMessage().getInt(DESTINATION)); + } + + return mDestinationTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationTalkgroup()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupVoiceChannelGrant.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupVoiceChannelGrant.java new file mode 100644 index 00000000..40c19117 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/grant/TalkgroupVoiceChannelGrant.java @@ -0,0 +1,155 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant; + +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.DMRSyncPattern; +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.mbc.MBCContinuationBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Channel Grant - Voice - Talkgroup + */ +public class TalkgroupVoiceChannelGrant extends ChannelGrant +{ + private static final int EMERGENCY_FLAG = 30; + + private List mIdentifiers; + private RadioIdentifier mSourceRadio; + private TalkgroupIdentifier mDestinationTalkgroup; + + /** + * Constructs a single-block CSBK 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 TalkgroupVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Constructs a multi-block MBC instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + * @param multiBlock containing absolute frequency parameters + */ + public TalkgroupVoiceChannelGrant(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, + SlotType slotType, long timestamp, int timeslot, MBCContinuationBlock multiBlock) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot, multiBlock); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + + if(isEmergency()) + { + sb.append(" EMERGENCY"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio()); + sb.append(" TO:").append(getDestinationTalkgroup()); + sb.append(" CHAN:").append(getChannel()); + return sb.toString(); + } + + /** + * Indicates if the emergency flag is set + */ + public boolean isEmergency() + { + return getMessage().get(EMERGENCY_FLAG); + } + + /** + * Source radio identifier + * @return + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE)); + } + + return mSourceRadio; + } + + /** + * Destination radio identifier + */ + public TalkgroupIdentifier getDestinationTalkgroup() + { + if(mDestinationTalkgroup == null) + { + mDestinationTalkgroup = DMRTalkgroup.create(getMessage().getInt(DESTINATION)); + } + + return mDestinationTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getChannel()); + mIdentifiers.add(getSourceRadio()); + mIdentifiers.add(getDestinationTalkgroup()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ConfirmedDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ConfirmedDataHeader.java new file mode 100644 index 00000000..83714ce5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ConfirmedDataHeader.java @@ -0,0 +1,115 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Confirmed Data Header + */ +public class ConfirmedDataHeader extends OctetDataHeader +{ + private static final int RE_SYNCHRONIZE_FLAG = 72; + private static final int[] SEND_SEQUENCE_NUMBER = new int[]{73, 74, 75}; + + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public ConfirmedDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" CONFIRMED DATA HEADER"); + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + if(isReSynchronize()) + { + sb.append(" (RESYNC)"); + } + sb.append(" SEND SEQUENCE NUMBER:").append(getSendSequenceNumber()); + sb.append(" FRAGMENT SEQUENCE NUMBER:").append(getFragmentSequenceNumber()); + if(isFinalFragment()) + { + sb.append("(FINAL)"); + } + sb.append(" PAD OCTETS:").append(getPadOctetCount()); + return sb.toString(); + } + + /** + * Send sequence number + */ + public int getSendSequenceNumber() + { + return getMessage().getInt(SEND_SEQUENCE_NUMBER); + } + + /** + * Indicates if the receiver should re-synchronize by accepting this packet and its fragment sequence number as a + * valid fragment and avoid ignoring this packet as a duplicate when the fragment sequence number was previously + * received. + * @return true if resynchronized is asserted + */ + public boolean isReSynchronize() + { + return getMessage().get(RE_SYNCHRONIZE_FLAG); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java new file mode 100644 index 00000000..fbac812f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java @@ -0,0 +1,101 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.type.DataPacketFormat; + +/** + * Data Header message. + */ +public class DataHeader extends HeaderMessage +{ + private static final int[] DATA_PACKET_FORMAT = new int[]{4, 5, 6, 7}; + + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" DATA HEADER"); + sb.append(" FORMAT:").append(getDataPacketFormat()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Data packet format for the specified message + * @param message + * @return + */ + public static DataPacketFormat getDataPacketFormat(CorrectedBinaryMessage message) + { + return DataPacketFormat.fromValue(message.getInt(DATA_PACKET_FORMAT)); + } + + /** + * Data Packet Format for this message + */ + public DataPacketFormat getDataPacketFormat() + { + return getDataPacketFormat(getMessage()); + } + + /** + * Indicates if this is a header for a confirmed packet sequence + */ + public boolean isConfirmedData() + { + return getDataPacketFormat() == DataPacketFormat.CONFIRMED_DATA_PACKET; + } + + /** + * Indicates if this is a header for an unconfirmed packet sequence + */ + public boolean isUnconfirmedData() + { + return getDataPacketFormat() == DataPacketFormat.UNCONFIRMED_DATA_PACKET; + } + +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DefinedShortDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DefinedShortDataHeader.java new file mode 100644 index 00000000..89828448 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DefinedShortDataHeader.java @@ -0,0 +1,141 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.type.DefinedDataFormat; + +import java.util.ArrayList; +import java.util.List; + +/** + * Defined Short Data Header + */ +public class DefinedShortDataHeader extends PacketSequenceHeader +{ + private static final int[] BLOCKS_TO_FOLLOW = new int[]{2, 3, 12, 13, 14, 15}; + private static final int[] DEFINED_DATA_FORMAT = new int[]{64, 65, 66, 67, 68, 69}; + private static final int RESYNCHRONIZE_FLAG = 70; + private static final int FULL_MESSAGE_FLAG = 71; + private static final int[] BIT_PADDING = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public DefinedShortDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(isResponseRequested() ? " CONFIRMED" : " UNCONFIRMED"); + sb.append(" DEFINED SHORT DATA HEADER"); + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" FORMAT:").append(getDefinedDataFormat()); + sb.append(" ").append(getServiceAccessPoint()); + if(isFullMessage()) + { + sb.append(" FULL MESSAGE"); + } + if(isResynchronize()) + { + sb.append(" RESYNCHRONIZE"); + } + sb.append(" BIT PADDING:").append(getBitPadding()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + return sb.toString(); + } + + /** + * Format for the data payload for this packet sequence. + */ + public DefinedDataFormat getDefinedDataFormat() + { + return DefinedDataFormat.fromValue(getMessage().getInt(DEFINED_DATA_FORMAT)); + } + + /** + * Indicates if this is a full message + */ + public boolean isFullMessage() + { + return getMessage().get(FULL_MESSAGE_FLAG); + } + + /** + * Indicates if this is resynchronized data + */ + public boolean isResynchronize() + { + return getMessage().get((RESYNCHRONIZE_FLAG)); + } + + /** + * Bit padding value. + */ + public int getBitPadding() + { + return getMessage().getInt(BIT_PADDING); + } + + /** + * Number of appended blocks + */ + @Override + public int getBlocksToFollow() + { + return getMessage().getInt(BLOCKS_TO_FOLLOW); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java new file mode 100644 index 00000000..8558f340 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java @@ -0,0 +1,48 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Header data message. + */ +public class HeaderMessage extends DataMessageWithLinkControl +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public HeaderMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/MBCHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/MBCHeader.java new file mode 100644 index 00000000..d6bb0685 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/MBCHeader.java @@ -0,0 +1,115 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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 io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.Collections; +import java.util.List; + +/** + * Multi-Block Header for Multiple Block CSBK Messages + */ +public class MBCHeader extends CSBKMessage +{ + /** + * 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 MBCHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * CRC Mask to use for this message + */ + protected int getCRCMask() + { + return 0xAAAA; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("CC:").append(getSlotType().getColorCode()); + + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + + sb.append(" MULTI-BLOCK CSBK HEADER"); + + Vendor vendor = getVendor(); + + if(vendor != Vendor.STANDARD) + { + if(vendor == Vendor.UNKNOWN) + { + sb.append(" VENDOR:UNKNOWN (").append(getVendorID()).append(")"); + } + else + { + sb.append(" VENDOR:").append(vendor); + } + } + + Opcode opcode = getOpcode(); + + if(opcode == Opcode.UNKNOWN) + { + sb.append(" UNKNOWN CSBKO:").append(getOpcodeValue()); + } + else + { + sb.append(" ").append(opcode); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/OctetDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/OctetDataHeader.java new file mode 100644 index 00000000..04c4c907 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/OctetDataHeader.java @@ -0,0 +1,86 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Packet sequence header that contains octet (byte) data + * + * Note: this is the base header for Confirmed, Unconfirmed and Short Data + */ +public abstract class OctetDataHeader extends PacketSequenceHeader +{ + private static final int[] PAD_OCTET_COUNT = new int[]{3, 12, 13, 14, 15}; + private static final int FINAL_FRAGMENT_FLAG = 64; + private static final int[] BLOCKS_TO_FOLLOW = new int[]{65, 66, 67, 68, 69, 70, 71}; + private static final int[] FRAGMENT_SEQUENCE_NUMBER = new int[]{76, 77, 78, 79}; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public OctetDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Fragment sequence number + */ + public int getFragmentSequenceNumber() + { + return getMessage().getInt(FRAGMENT_SEQUENCE_NUMBER); + } + + + /** + * Number of data blocks to follow + */ + public int getBlocksToFollow() + { + return getMessage().getInt(BLOCKS_TO_FOLLOW); + } + + /** + * Indicates if this is the final fragment in a fragment sequence + */ + public boolean isFinalFragment() + { + return getMessage().get(FINAL_FRAGMENT_FLAG); + } + + /** + * Number of octets (bytes) that are padded onto the last packet fragment to form a complete fragment. + */ + public int getPadOctetCount() + { + return getMessage().getInt(PAD_OCTET_COUNT); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PacketSequenceHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PacketSequenceHeader.java new file mode 100644 index 00000000..b862343c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PacketSequenceHeader.java @@ -0,0 +1,127 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.type.ServiceAccessPoint; + +/** + * Packet Sequence Data Header + */ +public abstract class PacketSequenceHeader extends DataHeader +{ + private static final int RADIO_TALKGROUP_FLAG = 0; + private static final int RESPONSE_REQUESTED = 1; + private static final int[] SERVICE_ACCESS_POINT = new int[]{8, 9, 10, 11}; + private static final int[] DESTINATION_IDENTIFIER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_RADIO = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + + private IntegerIdentifier mDestinationLLID; + private RadioIdentifier mSourceLLID; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public PacketSequenceHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Indicates if a response to this packet is requested to indicate if this is a CONFIRMED(true) or UNCONFIRMED (false) + * message type. + * @return true for confirmed and false for unconfirmed. + */ + public boolean isResponseRequested() + { + return getMessage().get(RESPONSE_REQUESTED); + } + + /** + * Number of data blocks to follow + */ + public abstract int getBlocksToFollow(); + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * Destination Logical Link ID + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + } + + return mDestinationLLID; + } + + + /** + * Source Logical Link ID + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO)); + } + + return mSourceLLID; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ProprietaryDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ProprietaryDataHeader.java new file mode 100644 index 00000000..2f40f045 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ProprietaryDataHeader.java @@ -0,0 +1,111 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.type.ServiceAccessPoint; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Proprietary Data Header + */ +public class ProprietaryDataHeader extends DataHeader +{ + private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3}; + private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public ProprietaryDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" PROPRIETARY DATA HEADER"); + sb.append(" VENDOR:").append(getVendor()); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Optional packet prefix. + * @return packet prefix fragment or null. + */ + public CorrectedBinaryMessage getPacketPrefix() + { + return null; + } + + /** + * Utility method to lookup the vendor from a CSBK message + * @param message containing CSBK bits + * @return vendor + */ + public static Vendor getVendor(BinaryMessage message) + { + return Vendor.fromValue(message.getInt(VENDOR)); + } + + /** + * Vendor for this message + */ + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/RawShortDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/RawShortDataHeader.java new file mode 100644 index 00000000..0cc0ee1a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/RawShortDataHeader.java @@ -0,0 +1,101 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Raw Short Data Header + */ +public class RawShortDataHeader extends ShortDataHeader +{ + private static final int RESYNCHRONIZE_FLAG = 70; + private static final int FULL_MESSAGE_FLAG = 71; + private static final int[] BIT_PADDING = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public RawShortDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" RAW SHORT DATA"); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" SOURCE PORT:").append(getSourcePort()); + sb.append(" DESTINATION PORT:").append(getDestinationPort()); + if(isFullMessage()) + { + sb.append(" FULL MESSAGE"); + } + if(isResynchronize()) + { + sb.append(" RESYNCHRONIZE"); + } + sb.append(" BIT PADDING:").append(getBitPadding()); + sb.append(" APPENDED BLOCKS:").append(getAppendedBlocks()); + return sb.toString(); + } + + /** + * Indicates if this is a full message + */ + public boolean isFullMessage() + { + return getMessage().get(FULL_MESSAGE_FLAG); + } + + /** + * Indicates if this is resynchronized data + */ + public boolean isResynchronize() + { + return getMessage().get((RESYNCHRONIZE_FLAG)); + } + + /** + * Bit padding value. + */ + public int getBitPadding() + { + return getMessage().getInt(BIT_PADDING); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ResponseDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ResponseDataHeader.java new file mode 100644 index 00000000..3cece02b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ResponseDataHeader.java @@ -0,0 +1,168 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +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.module.decode.dmr.DMRSyncPattern; +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.type.ServiceAccessPoint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Response Data Header + */ +public class ResponseDataHeader extends DataHeader +{ + private static final int RADIO_TALKGROUP_FLAG = 0; + private static final int[] SERVICE_ACCESS_POINT = new int[]{8, 9, 10, 11}; + private static final int[] DESTINATION_IDENTIFIER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_RADIO = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] BLOCKS_TO_FOLLOW = new int[]{65, 66, 67, 68, 69, 70, 71}; + private static final int[] CLASS_TYPE_STATUS = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mDestinationLLID; + private RadioIdentifier mSourceLLID; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public ResponseDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" RESPONSE DATA HEADER"); + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" CLASS/TYPE/STATUS:").append(getClassTypeStatus()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + return sb.toString(); + } + + /** + * Class, Type and Status of response. + * + * TODO: create an enumeration for this value - see TS 102 361-3 for definitions + * @return + */ + public int getClassTypeStatus() + { + return getMessage().getInt(CLASS_TYPE_STATUS); + } + + /** + * Number of data blocks to follow + */ + public int getBlocksToFollow() + { + return getMessage().getInt(BLOCKS_TO_FOLLOW); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * Destination Logical Link ID + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + } + + return mDestinationLLID; + } + + + /** + * Source Logical Link ID + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO)); + } + + return mSourceLLID; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ShortDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ShortDataHeader.java new file mode 100644 index 00000000..5e628623 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/ShortDataHeader.java @@ -0,0 +1,170 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +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.module.decode.dmr.DMRSyncPattern; +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.type.ServiceAccessPoint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Short Data Header + */ +public abstract class ShortDataHeader extends OctetDataHeader +{ + private static final int RADIO_TALKGROUP_FLAG = 0; + private static final int[] SERVICE_ACCESS_POINT = new int[]{8, 9, 10, 11}; + private static final int[] APPENDED_BLOCKS = new int[]{2, 3, 12, 13, 14, 15}; + private static final int[] DESTINATION_IDENTIFIER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_RADIO = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] SOURCE_PORT = new int[]{64, 65, 66}; + private static final int[] DESTINATION_PORT = new int[]{67, 68, 69}; + + private IntegerIdentifier mDestinationLLID; + private RadioIdentifier mSourceLLID; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public ShortDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + /** + * Source port + */ + public int getSourcePort() + { + return getMessage().getInt(SOURCE_PORT); + } + + /** + * Destination port + */ + public int getDestinationPort() + { + return getMessage().getInt(DESTINATION_PORT); + } + + /** + * Indicates the number of appended blocks. + * + * Note: this value is used to determine if the short data header is a status/precoded header (blocks = 0) or the + * short data header is a raw short data header. + * @param message containing the appended blocks value + * @return appended blocks count + */ + public static int getAppendedBlocks(CorrectedBinaryMessage message) + { + return message.getInt(APPENDED_BLOCKS); + } + + /** + * Number of appended blocks + */ + public int getAppendedBlocks() + { + return getAppendedBlocks(getMessage()); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * Destination Logical Link ID + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + } + + return mDestinationLLID; + } + + + /** + * Source Logical Link ID + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO)); + } + + return mSourceLLID; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/StatusDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/StatusDataHeader.java new file mode 100644 index 00000000..8bea86bb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/StatusDataHeader.java @@ -0,0 +1,208 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +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.module.decode.dmr.DMRSyncPattern; +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.identifier.DMRUnitStatus; +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.type.ServiceAccessPoint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Status/Precoded Short Data Header + * + * Note: this class does not extend ShortDataHeader because it has no appended blocks and therefore is not a packet + * sequence header. It really doesn't make sense that this message is a header at all, since it is standalone and + * doesn't precede any data block sequence. + */ +public class StatusDataHeader extends DataHeader +{ + private static final int RADIO_TALKGROUP_FLAG = 0; + private static final int[] SERVICE_ACCESS_POINT = new int[]{8, 9, 10, 11}; + private static final int[] APPENDED_BLOCKS = new int[]{2, 3, 12, 13, 14, 15}; + private static final int[] DESTINATION_IDENTIFIER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_RADIO = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] SOURCE_PORT = new int[]{64, 65, 66}; + private static final int[] DESTINATION_PORT = new int[]{67, 68, 69}; + private static final int[] STATUS = new int[]{70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private DMRUnitStatus mUnitStatus; + private IntegerIdentifier mDestinationLLID; + private RadioIdentifier mSourceLLID; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public StatusDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" STATUS:").append(getUnitStatus()); + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" SOURCE PORT:").append(getSourcePort()); + sb.append(" DESTINATION PORT:").append(getDestinationPort()); + return sb.toString(); + } + + /** + * Source port + */ + public int getSourcePort() + { + return getMessage().getInt(SOURCE_PORT); + } + + /** + * Destination port + */ + public int getDestinationPort() + { + return getMessage().getInt(DESTINATION_PORT); + } + + /** + * Indicates the number of appended blocks. + * + * Note: this value is used to determine if the short data header is a status/precoded header (blocks = 0) or the + * short data header is a raw short data header. + * @param message containing the appended blocks value + * @return appended blocks count + */ + public static int getAppendedBlocks(CorrectedBinaryMessage message) + { + return message.getInt(APPENDED_BLOCKS); + } + + /** + * Number of appended blocks + */ + public int getAppendedBlocks() + { + return getAppendedBlocks(getMessage()); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * Destination Logical Link ID + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + } + + return mDestinationLLID; + } + + + /** + * Source Logical Link ID + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO)); + } + + return mSourceLLID; + } + /** + * Status value. + * @return 10-bit status or precoded message value. + */ + public DMRUnitStatus getUnitStatus() + { + if(mUnitStatus == null) + { + mUnitStatus = DMRUnitStatus.create(getMessage().getInt(STATUS)); + } + + return mUnitStatus; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + mIdentifiers.add(getUnitStatus()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java new file mode 100644 index 00000000..939e446c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java @@ -0,0 +1,235 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +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.module.decode.dmr.DMRSyncPattern; +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.Opcode; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint; +import io.github.dsheirer.module.decode.dmr.message.type.UnifiedDataTransportFormat; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unified Data Transport Header + */ +public class UDTHeader extends DataHeader +{ + private static final int RADIO_TALKGROUP_FLAG = 0; + private static final int[] SERVICE_ACCESS_POINT = new int[]{8, 9, 10, 11}; + private static final int[] UDT_FORMAT = new int[]{3, 12, 13, 14, 15}; + + private static final int[] DESTINATION_IDENTIFIER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_RADIO = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int[] PAD_NIBBLE = new int[]{64, 65, 66, 67, 68}; + private static final int[] UAB = new int[]{70, 71}; + private static final int SUPPLEMENTARY_DATA_FLAG = 72; + private static final int PROTECT_FLAG = 73; + private static final int[] OPCODE = new int[]{74, 75, 76, 77, 78, 79}; + + private IntegerIdentifier mDestinationLLID; + private RadioIdentifier mSourceLLID; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public UDTHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" UDT HEADER"); + if(isSupplementaryData()) + { + sb.append(" SUPPLEMENTARY DATA"); + } + else + { + sb.append(" SHORT DATA"); + } + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" FORMAT:").append(getFormat()); + Opcode opcode = getOpcode(); + + if(opcode == Opcode.UNKNOWN) + { + sb.append(" OPCODE:").append(getOpcodeValue()); + } + else + { + sb.append(" OPCODE:").append(getOpcode()); + } + sb.append(" PAD NIBBLES:").append(getPadNibbleCount()); + sb.append(" ").append(getServiceAccessPoint()); + return sb.toString(); + } + + /** + * Indicates if the header contains supplementary data (true) or short data (false) + * @param message containing the flag + * @return value + */ + public static boolean isSupplementaryData(CorrectedBinaryMessage message) + { + return message.get(SUPPLEMENTARY_DATA_FLAG); + } + + /** + * Indicates if the header contains supplementary data (true) or short data (false) + */ + public boolean isSupplementaryData() + { + return isSupplementaryData(getMessage()); + } + + /** + * Count of nibbles that were padded onto the end of the message to make a full data block. + */ + public int getPadNibbleCount() + { + return getMessage().getInt(PAD_NIBBLE); + } + + /** + * Utility method to lookup the Opcode for this UDT header message + * @param message containing a UDT header + * @return opcode for the sequence + */ + public static Opcode getOpcode(CorrectedBinaryMessage message) + { + return Opcode.fromValue(message.getInt(OPCODE), Vendor.STANDARD); + } + + /** + * Value of the opcode for this message + */ + public int getOpcodeValue() + { + return getMessage().getInt(OPCODE); + } + + /** + * Opcode for this UDT header + */ + public Opcode getOpcode() + { + return getOpcode(getMessage()); + } + + /** + * Data format + */ + public UnifiedDataTransportFormat getFormat() + { + return UnifiedDataTransportFormat.fromValue(getMessage().getInt(UDT_FORMAT)); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * Destination Logical Link ID + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(getMessage().get(RADIO_TALKGROUP_FLAG)) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_IDENTIFIER)); + } + } + + return mDestinationLLID; + } + + + /** + * Source Logical Link ID + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO)); + } + + return mSourceLLID; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UnconfirmedDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UnconfirmedDataHeader.java new file mode 100644 index 00000000..11c89810 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UnconfirmedDataHeader.java @@ -0,0 +1,88 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unconfirmed Data Header + */ +public class UnconfirmedDataHeader extends OctetDataHeader +{ + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public UnconfirmedDataHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + if(!isValid()) + { + sb.append(" [CRC ERROR]"); + } + sb.append(" UNCONFIRMED DATA HEADER"); + sb.append(" FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlocksToFollow()); + sb.append(" FRAGMENT SEQUENCE NUMBER:").append(getFragmentSequenceNumber()); + if(isFinalFragment()) + { + sb.append("(FINAL)"); + } + sb.append(" PAD OCTETS:").append(getPadOctetCount()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationLLID()); + mIdentifiers.add(getSourceLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java new file mode 100644 index 00000000..b4435000 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java @@ -0,0 +1,57 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Voice Header data message. + */ +public class VoiceHeader extends HeaderMessage +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public VoiceHeader(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" VOICE HEADER"); + sb.append(" ").append(getLCMessage()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/hytera/HyteraProprietaryDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/hytera/HyteraProprietaryDataHeader.java new file mode 100644 index 00000000..905108b6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/hytera/HyteraProprietaryDataHeader.java @@ -0,0 +1,78 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header.hytera; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.header.ProprietaryDataHeader; + +/** + * Hytera Proprietary Data Header + */ +public class HyteraProprietaryDataHeader extends ProprietaryDataHeader +{ + private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public HyteraProprietaryDataHeader(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(); + sb.append("HYTERA PROPRIETARY DATA HEADER"); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" UNK1:").append(getUnknown1()); + sb.append(" UNK2:").append(getUnknown2()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Unknown 8-bit field. This is likely the encryption type identifier. + */ + public int getUnknown1() + { + return getMessage().getInt(UNKNOWN_1); + } + + /** + * Unknown 8-bit field. This is likely the encryption key identifier. + */ + public int getUnknown2() + { + return getMessage().getInt(UNKNOWN_2); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MNISProprietaryDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MNISProprietaryDataHeader.java new file mode 100644 index 00000000..a0dd66cf --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MNISProprietaryDataHeader.java @@ -0,0 +1,167 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header.motorola; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.header.ProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Motorola MOTOTRBO Network Interface Service (MNIS) Data Header for SAP:1 + * + * See: https://cwh050.blogspot.com/2019/08/what-does-mnis-do.html + */ +public class MNISProprietaryDataHeader extends ProprietaryDataHeader +{ + private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3}; + private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + //NOTE: the following field definitions are speculative ... + private static final int[] UNKNOWN_02 = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] MESSAGE_TYPE = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] APPLICATION_TYPE = new int[]{32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] PACKET_NUMBER = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int PACKET_PREFIX_START = 56; + private static final int PACKET_PREFIX_END = PACKET_PREFIX_START + 24; + + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public MNISProprietaryDataHeader(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(); + sb.append("MOTOROLA MNIS HEADER"); + if(getApplicationType() == ApplicationType.UNKNOWN) + { + sb.append(" APPLICATION TYPE:0x").append(Integer.toHexString(getApplicationTypeNumber()).toUpperCase()); + } + else + { + sb.append(" APPLICATION:").append(getApplicationType()); + } + sb.append(" PACKET:").append(getPacketNumber()); + sb.append(" PACKET PREFIX:").append(getPacketPrefix().toHexString()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Utility method to lookup the vendor from a CSBK message + * + * @param message containing CSBK bits + * @return vendor + */ + public static Vendor getVendor(BinaryMessage message) + { + return Vendor.fromValue(message.getInt(VENDOR)); + } + + /** + * Vendor for this message + */ + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } + + /** + * MNIS Application Type lookup utility + * @param message + * @return + */ + public static ApplicationType getApplicationType(CorrectedBinaryMessage message) + { + return ApplicationType.fromValue(message.getInt(APPLICATION_TYPE)); + } + + /** + * MNIS Application Type + */ + public ApplicationType getApplicationType() + { + return getApplicationType(getMessage()); + } + + /** + * Numeric value of the application type field + * @param message containing the application type field + * @return value + */ + public static int getApplicationTypeValue(CorrectedBinaryMessage message) + { + return message.getInt(APPLICATION_TYPE); + } + + /** + * Numeric value for the application type field + */ + public int getApplicationTypeNumber() + { + return getMessage().getInt(APPLICATION_TYPE); + } + + public int getPacketNumber() + { + return getMessage().getInt(PACKET_NUMBER); + } + + /** + * Access the first 3 octets/bytes of the packet sequence contained in this header + */ + public CorrectedBinaryMessage getPacketPrefix() + { + return getMessage().getSubMessage(PACKET_PREFIX_START, PACKET_PREFIX_END); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MotorolaProprietaryDataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MotorolaProprietaryDataHeader.java new file mode 100644 index 00000000..4e0070df --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/motorola/MotorolaProprietaryDataHeader.java @@ -0,0 +1,97 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.header.motorola; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +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.header.ProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Motorola Proprietary Data Header + */ +public class MotorolaProprietaryDataHeader extends ProprietaryDataHeader +{ + private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3}; + private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public MotorolaProprietaryDataHeader(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(); + sb.append("MOTOROLA PROPRIETARY DATA HEADER"); + sb.append(" ").append(getServiceAccessPoint()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Utility method to lookup the vendor from a CSBK message + * @param message containing CSBK bits + * @return vendor + */ + public static Vendor getVendor(BinaryMessage message) + { + return Vendor.fromValue(message.getInt(VENDOR)); + } + + /** + * Vendor for this message + */ + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Service access point for the specified message + */ + public static ServiceAccessPoint getServiceAccessPoint(CorrectedBinaryMessage message) + { + return ServiceAccessPoint.fromValue(message.getInt(SERVICE_ACCESS_POINT)); + } + + /** + * Service access point for this message + */ + public ServiceAccessPoint getServiceAccessPoint() + { + return getServiceAccessPoint(getMessage()); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessage.java new file mode 100644 index 00000000..e43a8c87 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessage.java @@ -0,0 +1,76 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.DMRMessage; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.List; + +/** + * Base Link Control class + */ +public abstract class LCMessage extends DMRMessage +{ + /** + * Constructs an instance + * @param message containing link control data + */ + public LCMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + /** + * Link control opcode + */ + public abstract LCOpcode getOpcode(); + + /** + * Link control vendor (feature ID) + */ + public abstract Vendor getVendor(); + + /** + * Access the identifiers contained in the link control message + */ + public abstract List getIdentifiers(); + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(isValid()) + { + sb.append("LINK CONTROL "); + sb.append(" ").append(getOpcode()); + sb.append(" ").append(getMessage().toHexString()); + } + else + { + sb.append("INVALID LINK CONTROL"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java new file mode 100644 index 00000000..2f9f4acf --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java @@ -0,0 +1,174 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.CRCDMR; +import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TerminatorData; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMessage; +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.CapacityPlusGroupVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel; +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.HyteraXPTChannel; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.NullMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.UnknownShortLCMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Link control factory class for creating both short and full link control messages + */ +public class LCMessageFactory +{ + private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class); + private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR(); + private static int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99; + private static int VOICE_LINK_CONTROL_CRC_MASK = 0x96; + + /** + * Creates a full link control message + * @param message bits + * @param timestamp of the original message + * @param timeslot of the original message + * @param isTerminator set to true if this is LC for a terminator data burst. + * @return message class + */ + public static FullLCMessage createFull(CorrectedBinaryMessage message, long timestamp, int timeslot, boolean isTerminator) + { + if(message == null) + { + throw new IllegalArgumentException("Message cannot be null"); + } + + //RS(12,9,4) can correct up to floor(4/2) = 1 bit error. + boolean corrected = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, + isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK); + + LCOpcode opcode = FullLCMessage.getOpcode(message); + + FullLCMessage flc = null; + + switch(opcode) + { + case FULL_STANDARD_GROUP_VOICE_CHANNEL_USER: + flc = new GroupVoiceChannelUser(message, timestamp, timeslot); + break; + case FULL_STANDARD_UNIT_TO_UNIT_VOICE_CHANNEL_USER: + flc = new UnitToUnitVoiceChannelUser(message, timestamp, timeslot); + break; + case FULL_STANDARD_GPS_INFO: + flc = new GPSInformation(message, timestamp, timeslot); + break; + case FULL_STANDARD_TERMINATOR_DATA: + flc = new TerminatorData(message, timestamp, timeslot); + break; + case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER: + flc = new CapacityPlusGroupVoiceChannelUser(message, timestamp, timeslot); + break; + case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER: + flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot); + break; + + case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER: + flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot); + break; + case FULL_HYTERA_UNIT_TO_UNIT_VOICE_CHANNEL_USER: + flc = new HyteraUnitToUnitVoiceChannelUser(message, timestamp, timeslot); + break; + case FULL_HYTERA_TERMINATOR: + flc = new HyteraTerminator(message, timestamp, timeslot); + break; + case FULL_STANDARD_TALKER_ALIAS_HEADER: + case FULL_STANDARD_TALKER_ALIAS_BLOCK_1: + case FULL_STANDARD_TALKER_ALIAS_BLOCK_2: + case FULL_STANDARD_TALKER_ALIAS_BLOCK_3: + default: + flc = new UnknownFullLCMessage(message, timestamp, timeslot); + break; + } + + flc.setValid(corrected); + return flc; + } + + /** + * Creates a short link control message + * @param message bits + * @return message class + */ + public static ShortLCMessage createShort(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + LCOpcode opcode = ShortLCMessage.getOpcode(message); + + ShortLCMessage slc = null; + + switch(opcode) + { + case SHORT_STANDARD_NULL_MESSAGE: + slc = new NullMessage(message, timestamp, timeslot); + break; + case SHORT_STANDARD_ACTIVITY_UPDATE:; + slc = new ActivityUpdateMessage(message, timestamp, timeslot); + break; + case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION: + slc = new CapacityPlusRestChannel(message, timestamp, timeslot); + break; + case SHORT_CONNECT_PLUS_CONTROL_CHANNEL: + slc = new ConnectPlusControlChannel(message, timestamp, timeslot); + break; + case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL: + slc = new ConnectPlusTrafficChannel(message, timestamp, timeslot); + break; + case SHORT_STANDARD_CONTROL_CHANNEL_SYSTEM_PARAMETERS: + slc = new ControlChannelSystemParameters(message, timestamp, timeslot); + break; + case SHORT_HYTERA_XPT_CHANNEL: + case SHORT_STANDARD_XPT_CHANNEL: + slc = new HyteraXPTChannel(message, timestamp, timeslot); + break; + case SHORT_STANDARD_TRAFFIC_CHANNEL_SYSTEM_PARAMETERS: + case SHORT_STANDARD_UNKNOWN: + default: + slc = new UnknownShortLCMessage(message, timestamp, timeslot); + break; + } + + boolean valid = CRCDMR.crc8(message, 36) == 0; + +// System.out.println("SLC MSG:" + message.toHexString() + " CRC-8 RESIDUAL: " + CRCDMR.crc8(message, 36)); +// + slc.setValid(valid); + + return slc; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java new file mode 100644 index 00000000..0f694886 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java @@ -0,0 +1,195 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc; + +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.EnumMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * Full and Short Link Control Opcode enumeration + * + * ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2 + * ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2 + */ +public enum LCOpcode +{ + FULL_STANDARD_GROUP_VOICE_CHANNEL_USER(Vendor.STANDARD,true, 0, "GROUP VOICE CHANNEL USER"), + FULL_STANDARD_UNIT_TO_UNIT_VOICE_CHANNEL_USER(Vendor.STANDARD,true, 3, "UNIT-TO-UNIT VOICE CHANNEL USER"), + FULL_STANDARD_TALKER_ALIAS_HEADER(Vendor.STANDARD,true, 4, "TALKER ALIAS HEADER"), + FULL_STANDARD_TALKER_ALIAS_BLOCK_1(Vendor.STANDARD,true, 5, "TALKER ALIAS BLOCK 1"), + FULL_STANDARD_TALKER_ALIAS_BLOCK_2(Vendor.STANDARD,true, 6, "TALKER ALIAS BLOCK 2"), + FULL_STANDARD_TALKER_ALIAS_BLOCK_3(Vendor.STANDARD,true, 7, "TALKER ALIAS BLOCK 3"), + FULL_STANDARD_GPS_INFO(Vendor.STANDARD,true, 8, "GPS INFO"), + FULL_STANDARD_TERMINATOR_DATA(Vendor.STANDARD, true, 48, "TERMINATOR DATA"), + FULL_STANDARD_UNKNOWN(Vendor.STANDARD,true, -1, "FULL UNKNOWN"), + + FULL_CAPACITY_PLUS_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_HYTERA_GROUP_VOICE_CHANNEL_USER(Vendor.HYTERA_68, true, 0, "HYTERA GROUP VOICE CHANNEL USER"), + FULL_HYTERA_UNIT_TO_UNIT_VOICE_CHANNEL_USER(Vendor.HYTERA_68, true, 3, "HYTERA UNIT-TO-UNIT VOICE CHANNEL USER"), + FULL_HYTERA_TERMINATOR(Vendor.HYTERA_68, true, 48, "HYTERA TERMINATOR"), + + SHORT_STANDARD_NULL_MESSAGE(Vendor.STANDARD,false,0, "NULL MESSAGE"), + SHORT_STANDARD_ACTIVITY_UPDATE(Vendor.STANDARD,false,1, "ACTIVITY UPDATE"), + SHORT_STANDARD_CONTROL_CHANNEL_SYSTEM_PARAMETERS(Vendor.STANDARD,false,2, "CONTROL CHANNEL SYSTEM PARAMETERS"), + SHORT_STANDARD_TRAFFIC_CHANNEL_SYSTEM_PARAMETERS(Vendor.STANDARD,false,3, "TRAFFIC CHANNEL SYSTEM PARAMETERS"), + + //CAP+ SLCO 15 = Rest Channel https://forums.radioreference.com/threads/understanding-capacity-plus-trunking.209318/page-7 + SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION(Vendor.MOTOROLA_CAPACITY_PLUS, false,15, "REST CHANNEL NOTIFICATION"), + + SHORT_STANDARD_XPT_CHANNEL(Vendor.STANDARD, false, 8, "HYTERA XPT CHANNEL"), + SHORT_HYTERA_XPT_CHANNEL(Vendor.HYTERA_68, false, 8, "HYTERA XPT CHANNEL"), + + SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL(Vendor.STANDARD, false, 9, "TRAFFIC CHANNEL INFO"), + SHORT_CONNECT_PLUS_CONTROL_CHANNEL(Vendor.STANDARD, false, 10, "CONTROL CHANNEL INFO"), + + + SHORT_STANDARD_UNKNOWN(Vendor.STANDARD,false,-1, "SHORT UNKNOWN"); + + private Vendor mVendor; + private boolean mFull; + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param vendor of the message + * @param full to indicate if this is a full or short link control opcode + * @param value of the opcode + * @param label for the opcode + */ + LCOpcode(Vendor vendor, boolean full, int value, String label) + { + mVendor = vendor; + mFull = full; + mValue = value; + mLabel = label; + } + + /** + * Standard vendor, or the vendor of a custom opcode + */ + public Vendor getVendor() + { + return mVendor; + } + + /** + * Indicates if this entry is a full link control opcode + */ + public boolean isFull() + { + return mFull; + } + + /** + * Indicates if this entry is a short link control opcode + */ + public boolean isShort() + { + return !mFull; + } + + public int getValue() + { + return mValue; + } + + public String getLabel() + { + return mLabel; + } + + @Override + public String toString() + { + return getLabel(); + } + + /** + * Lookup map of opcode values + */ + private static final Map> FULL_LOOKUP_MAP = new EnumMap<>(Vendor.class); + private static final Map> SHORT_LOOKUP_MAP = new EnumMap<>(Vendor.class); + + static + { + for(LCOpcode opcode: LCOpcode.values()) + { + if(opcode.isFull()) + { + if(FULL_LOOKUP_MAP.containsKey(opcode.getVendor())) + { + FULL_LOOKUP_MAP.get(opcode.getVendor()).put(opcode.getValue(), opcode); + } + else + { + Map map = new TreeMap<>(); + map.put(opcode.getValue(), opcode); + FULL_LOOKUP_MAP.put(opcode.getVendor(), map); + } + } + else + { + if(SHORT_LOOKUP_MAP.containsKey(opcode.getVendor())) + { + SHORT_LOOKUP_MAP.get(opcode.getVendor()).put(opcode.getValue(), opcode); + } + else + { + Map map = new TreeMap<>(); + map.put(opcode.getValue(), opcode); + SHORT_LOOKUP_MAP.put(opcode.getVendor(), map); + } + } + } + } + + /** + * Lookup the opcode from the value + */ + public static LCOpcode fromValue(boolean full, int value, Vendor vendor) + { + if(full) + { + LCOpcode fullOpcode = FULL_STANDARD_UNKNOWN; + Map map = FULL_LOOKUP_MAP.get(vendor); + if(map != null && map.containsKey(value)) + { + fullOpcode = map.get(value); + } + return fullOpcode; + } + else + { + LCOpcode shortOpcode = SHORT_STANDARD_UNKNOWN; + Map map = SHORT_LOOKUP_MAP.get(vendor); + if(map != null && map.containsKey(value)) + { + shortOpcode = map.get(value); + } + return shortOpcode; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/AbstractVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/AbstractVoiceChannelUser.java new file mode 100644 index 00000000..635bd531 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/AbstractVoiceChannelUser.java @@ -0,0 +1,58 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; + +/** + * Any Voice Channel User link control message that contains service options. + * + * ETSI TS 102 361-2 7.1.1.2 + */ +public abstract class AbstractVoiceChannelUser extends FullLCMessage +{ + private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + + private ServiceOptions mServiceOptions; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public AbstractVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + /** + * Service options for the call + */ + public ServiceOptions getServiceOptions() + { + if(mServiceOptions == null) + { + mServiceOptions = new ServiceOptions(getMessage().getInt(SERVICE_OPTIONS)); + } + + return mServiceOptions; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java new file mode 100644 index 00000000..6f224799 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java @@ -0,0 +1,223 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.Hamming16; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory; +import io.github.dsheirer.module.decode.dmr.message.type.LCSS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Reassembles embedded signalling fragments into Full Link Control Messages + */ +public class FLCAssembler +{ + private final static Logger mLog = LoggerFactory.getLogger(FLCAssembler.class); + private CorrectedBinaryMessage mAssemblingMessage; + private int mFragmentCount = 0; + private long mTimestamp; + private int mTimeslot; + + /** + * Constructs an instance + */ + public FLCAssembler(int timeslot) + { + mTimeslot = timeslot; + } + + /** + * Processes the full link control fragment + * @return a full link control message or null if the message is not yet fully assembled + */ + public FullLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp) + { + mTimestamp = timestamp; + + FullLCMessage message = null; + + switch(lcss) + { + case FIRST_FRAGMENT: + message = dispatch(); + mAssemblingMessage = new CorrectedBinaryMessage(128); + add(fragment); + break; + case CONTINUATION_FRAGMENT: + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(128); + mAssemblingMessage.setPointer(32); + } + add(fragment); + break; + case LAST_FRAGMENT: + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(128); + mAssemblingMessage.setPointer(96); + } + add(fragment); + message = dispatch(); + break; + case SINGLE_FRAGMENT: + //TODO: this is the reverse channel signalling embedded in voice frame F + dispatch(); + default: + //Ignore + break; + } + + return message; + } + + /** + * Dispatches a fully-received messsage + */ + private FullLCMessage dispatch() + { + FullLCMessage message = null; + + if(mFragmentCount == 4) + { + message = decode(mAssemblingMessage, mTimestamp, mTimeslot); + } + + mAssemblingMessage = null; + mFragmentCount = 0; + return message; + } + + /** + * Decodes an assembled FLC message, performs error detection and correction and returns a constructed FLC message + */ + private static FullLCMessage decode(BinaryMessage message, long timestamp, int timeslot) + { + if(message != null) + { + boolean valid = true; + + CorrectedBinaryMessage descrambled = new CorrectedBinaryMessage(128); + int src; + descrambled.set(127, message.get(127)); + for(int i = 0; i < 127; i++) + { + src = (i * 8) % 127; + boolean x = message.get(src); + descrambled.set(i, message.get(src)); + } + + int errorCount = 0; + + for(int row = 0; row < 8; row++) + { + int rowErrorCount = Hamming16.checkAndCorrect(descrambled, row * 16); + errorCount += rowErrorCount; + + if(errorCount > 1) + { + valid = false; + } + } + + //If valid, check the column parity values + if(valid) + { + //Check the column parity values + for(int column = 0; column < 16; column++) + { + boolean parity = descrambled.get(column); + + for(int y = 1; y < 8; y++) + { + parity ^= descrambled.get(column + (y * 16)); + } + + if(parity) + { + valid = false; + } + } + } + + //Extract the message. Row 1 and 2 are 11 bits. Rows 3-7 are 10 bits. + CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(77); + int pointer = 0; + + for(int row = 0; row < 2; row++) + { + for(int column = 0; column < 11; column++) + { + extractedMessage.set(pointer++, descrambled.get(row * 16 + column)); + } + } + + for(int row = 2; row < 7; row++) + { + for(int column = 0; column < 10; column++) + { + extractedMessage.set(pointer++, descrambled.get(row * 16 + column)); + } + } + + extractedMessage.setCorrectedBitCount(errorCount); + FullLCMessage flco = LCMessageFactory.createFull(extractedMessage, timestamp, timeslot, false); + flco.setValid(valid); + return flco; + } + + return null; + } + + /** + * Adds the cach payload to the currently assembling message + */ + private void add(BinaryMessage payload) + { + if(payload == null || payload.size() != 32) + { + return; + } + + mFragmentCount++; + + try + { + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(96); + } + + for(int x = 0; x < 32; x++) + { + mAssemblingMessage.add(payload.get(x)); + } + } + catch(BitSetFullException bsfe) + { + //Something went wrong and we overfilled the bitset + dispatch(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java new file mode 100644 index 00000000..a3e40f9b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java @@ -0,0 +1,102 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Full Link Control message + */ +public abstract class FullLCMessage extends LCMessage +{ + private static final int ENCRYPTION_FLAG = 0; + private static final int RESERVED = 1; + private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7}; + private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs an instance + * @param message for link control payload + */ + public FullLCMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + /** + * Link control opcode for this message + */ + @Override + public LCOpcode getOpcode() + { + return getOpcode(getMessage()); + } + + /** + * Opcode numeric value + */ + public int getOpcodeValue() + { + return getMessage().getInt(OPCODE); + } + + /** + * Lookup the opcode for a full link control message + */ + public static LCOpcode getOpcode(CorrectedBinaryMessage message) + { + return LCOpcode.fromValue(true, message.getInt(OPCODE), getVendor(message)); + } + + /** + * Vendor or Feature Identifier for this link control message + */ + @Override + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Lookup the vendor for a full link control message + */ + public static Vendor getVendor(CorrectedBinaryMessage message) + { + return Vendor.fromValue(message.getInt(VENDOR)); + } + + /** + * Value for the vendor + */ + public int getVendorValue() + { + return getMessage().getInt(VENDOR); + } + + /** + * Indicates if the contents of this link control message are encrypted + */ + public boolean isEncrypted() + { + return getMessage().get(ENCRYPTION_FLAG); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GPSInformation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GPSInformation.java new file mode 100644 index 00000000..c9d7d06b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GPSInformation.java @@ -0,0 +1,119 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.location.LocationIdentifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation; +import io.github.dsheirer.module.decode.dmr.message.type.PositionError; +import org.apache.commons.math3.util.FastMath; + +import java.util.ArrayList; +import java.util.List; + +/** + * GPS Information + * + * ETSI TS 102 361-2 7.1.1.3 + */ +public class GPSInformation extends FullLCMessage +{ + private static final double LATITUDE_UNITS = 180.0 / FastMath.pow(2.0, 24.0); + private static final double LONGITUDE_UNITS = 360.0 / FastMath.pow(2.0, 25.0); + private static final int[] POSITION_ERROR = new int[]{20, 21, 22}; + private static final int LONGITUDE_START = 23; + private static final int LONGITUDE_END = 47; + private static final int LATITUDE_START = 48; + private static final int LATITUDE_END = 71; + + private DMRLocation mGPSLocation; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public GPSInformation(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("GPS LOCATION "); + sb.append(getGPSLocation().toString()); + sb.append(" POSITION ERROR:").append(getPositionError().toString()); + return sb.toString(); + } + + /** + * Position error report + */ + public PositionError getPositionError() + { + return PositionError.fromValue(getMessage().getInt(POSITION_ERROR)); + } + + public double getLatitude() + { + return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS; + } + + public double getLongitude() + { + return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS; + } + + /** + * GPS location as an identifier + */ + public LocationIdentifier getGPSLocation() + { + if(mGPSLocation == null) + { + mGPSLocation = DMRLocation.createFrom(getLatitude(), getLongitude()); + } + + return mGPSLocation; + } + + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getGPSLocation()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java new file mode 100644 index 00000000..f4125249 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java @@ -0,0 +1,113 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +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; + +/** + * Group Voice Channel User + * + * ETSI TS 102 361-2 7.1.1.1 + */ +public class GroupVoiceChannelUser extends AbstractVoiceChannelUser +{ + private static final int[] GROUP_ADDRESS = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] SOURCE_ADDRESS = 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 RadioIdentifier mRadio; + private TalkgroupIdentifier mTalkgroup; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public GroupVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC GROUP VOICE CHANNEL USER FM:"); + sb.append(getRadio()); + sb.append(" TO:").append(getTalkgroup()); + sb.append(" ").append(getServiceOptions()); + return sb.toString(); + } + + /** + * Source radio address + */ + public RadioIdentifier getRadio() + { + if(mRadio == null) + { + mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mRadio; + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TerminatorData.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TerminatorData.java new file mode 100644 index 00000000..b54733f8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TerminatorData.java @@ -0,0 +1,176 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +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.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Terminator Data Full Link Control Message + * + * See: TS 102 361-3 Paragraph 7.1.1.1 Terminator Data Link Control PDU + */ +public class TerminatorData extends FullLCMessage +{ + private static final int[] DESTINATION_LLID = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] SOURCE_LLID = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63}; + private static final int GROUP_INDIVIDUAL_FLAG = 64; + private static final int RESPONSE_REQUESTED = 65; + private static final int FULL_MESSAGE_FLAG = 66; + private static final int RESYNC_FLAG = 68; + private static final int[] SEND_SEQUENCE_NUMBER = new int[]{69, 70, 71}; + + private RadioIdentifier mSourceLLID; + private IntegerIdentifier mDestinationLLID; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + * @param timestamp + * @param timeslot + */ + public TerminatorData(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC ERROR " + getMessage().getCorrectedBitCount() + "] "); + } + + sb.append("FM:").append(getSourceLLID()); + sb.append(" TO:").append(getDestinationLLID()); + sb.append(isFullMessage() ? " COMPLETE" : " FRAGMENT"); + sb.append(" SEQUENCE:").append(getSendSequenceNumber()); + if(isResync()) + { + sb.append( " RESYNC"); + } + if(isResponseRequested()) + { + sb.append(" ACK:YES"); + } + + return sb.toString(); + } + + /** + * Destination or TO LLID identifier + */ + public IntegerIdentifier getDestinationLLID() + { + if(mDestinationLLID == null) + { + if(isGroupDestination()) + { + mDestinationLLID = DMRTalkgroup.create(getMessage().getInt(DESTINATION_LLID)); + } + else + { + mDestinationLLID = DMRRadio.createTo(getMessage().getInt(DESTINATION_LLID)); + } + } + + return mDestinationLLID; + } + + /** + * Indicates if the destination LLID is a talkgroup (true) or radio (false). + */ + public boolean isGroupDestination() + { + return getMessage().get(GROUP_INDIVIDUAL_FLAG); + } + + /** + * Source or FROM LLID identifier + */ + public RadioIdentifier getSourceLLID() + { + if(mSourceLLID == null) + { + mSourceLLID = DMRRadio.createFrom(getMessage().getInt(SOURCE_LLID)); + } + + return mSourceLLID; + } + + /** + * Indicates if a response is requested + */ + public boolean isResponseRequested() + { + return getMessage().get(RESPONSE_REQUESTED); + } + + /** + * Indicates if the transmitted data is a full message (or partial). + */ + public boolean isFullMessage() + { + return getMessage().get(FULL_MESSAGE_FLAG); + } + + /** + * Indicates if the transmitted message is asserting a resync for a sequence of partial fragments. + */ + public boolean isResync() + { + return getMessage().get(RESYNC_FLAG); + } + + /** + * Send sequence number for a sequence of partial fragments + */ + public int getSendSequenceNumber() + { + return getMessage().getInt(SEND_SEQUENCE_NUMBER); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSourceLLID()); + mIdentifiers.add(getDestinationLLID()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnitToUnitVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnitToUnitVoiceChannelUser.java new file mode 100644 index 00000000..e4682c45 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnitToUnitVoiceChannelUser.java @@ -0,0 +1,111 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit to Unit Voice Channel User + * + * ETSI TS 102 361-2 7.1.1.2 + */ +public class UnitToUnitVoiceChannelUser extends AbstractVoiceChannelUser +{ + private static final int[] TARGET_ADDRESS = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] SOURCE_ADDRESS = 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 RadioIdentifier mSourceRadio; + private RadioIdentifier mTargetRadio; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public UnitToUnitVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC UNIT TO UNIT VOICE CHANNEL USER FM:"); + sb.append(getSourceRadio()); + sb.append(" TO:").append(getTargetRadio()); + sb.append(" ").append(getServiceOptions()); + return sb.toString(); + } + + /** + * Source radio address + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + + /** + * Target radio address + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnknownFullLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnknownFullLCMessage.java new file mode 100644 index 00000000..c53123f2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/UnknownFullLCMessage.java @@ -0,0 +1,90 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.Collections; +import java.util.List; + +/** + * Unknown full link control message + */ +public class UnknownFullLCMessage extends FullLCMessage +{ + /** + * Constructs an instance + * + * @param message containing link control data + */ + public UnknownFullLCMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC UNKNOWN"); + LCOpcode opcode = getOpcode(); + + if(opcode != LCOpcode.FULL_STANDARD_UNKNOWN) + { + sb.append(" ").append(opcode); + } + else + { + sb.append(" OPCODE:").append(getOpcodeValue()); + } + + Vendor vendor = getVendor(); + + if(vendor != Vendor.STANDARD) + { + if(vendor == Vendor.UNKNOWN) + { + sb.append(" VENDOR:").append(getVendorValue()); + } + else + { + sb.append(" VENDOR:").append(vendor); + } + } + + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraFullLC.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraFullLC.java new file mode 100644 index 00000000..8d40334d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraFullLC.java @@ -0,0 +1,143 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; + +/** + * Hytera Full Link Control + */ +public abstract class HyteraFullLC extends FullLCMessage +{ + private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] FREE_REPEATER = new int[]{24, 25, 26, 27}; + private static final int[] PRIORITY_REPEATER = new int[]{28, 29, 30, 31}; + protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] UNKNOWN_1 = new int[]{72, 73, 74, 75}; + private static final int[] UNKNOWN_2 = new int[]{76, 77, 78, 79}; + + private ServiceOptions mServiceOptions; + private RadioIdentifier mSourceRadio; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public HyteraFullLC(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + /** + * Service options for the call + */ + public ServiceOptions getServiceOptions() + { + if(mServiceOptions == null) + { + mServiceOptions = new ServiceOptions(getMessage().getInt(SERVICE_OPTIONS)); + } + + return mServiceOptions; + } + + /** + * Indicates if all free repreaters are busy + */ + public boolean isAllChannelsBusy() + { + return getFreeRepeater() == 0; + } + + + /** + * Free repeater number. + * + * @return repeater, or 0 for all repeaters busy + */ + public int getFreeRepeater() + { + return getMessage().getInt(FREE_REPEATER); + } + + /** + * Indicates if there is a priority call on another repeater channel that users should monitor when their + * talkgroup matches the priority call hashed address + */ + public boolean hasPriorityCall() + { + return getPriorityCallRepeater() > 0; + } + + /** + * Indicates the repeater number that is forwarding a priority call + */ + public int getPriorityCallRepeater() + { + return getMessage().getInt(PRIORITY_REPEATER); + } + + /** + * Hashed address of priority call + */ + public String getPriorityCallHashedAddress() + { + return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase(); + } + + /** + * Unknown nibble one. This value remains consistent for the FLC between the Voice Header and the Terminator + * for calls. It does not seem to have correlation to the channel/repeater number + */ + public int getUnknownField1() + { + return getMessage().getInt(UNKNOWN_1); + } + + /** + * Unknown nibble two. For each talkgroup, this field shows one value for the Voice Header and the same value but + * bit-inverted in the Terminator. + */ + public int getUnknownField2() + { + return getMessage().getInt(UNKNOWN_2); + } + + /** + * Source radio address + */ + public RadioIdentifier getSourceRadio() + { + if(mSourceRadio == null) + { + mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mSourceRadio; + } + +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraGroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraGroupVoiceChannelUser.java new file mode 100644 index 00000000..737dc776 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraGroupVoiceChannelUser.java @@ -0,0 +1,106 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera; + +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.identifier.DMRTalkgroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera Group Voice Channel User + */ +public class HyteraGroupVoiceChannelUser extends HyteraFullLC +{ + private TalkgroupIdentifier mTalkgroup; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public HyteraGroupVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC HYTERA XPT GROUP VOICE CHANNEL USER FM:"); + sb.append(getSourceRadio()); + sb.append(" TO:").append(getTalkgroup()); + if(isAllChannelsBusy()) + { + sb.append(" ALL REPEATERS BUSY"); + } + else + { + sb.append(" FREE REPEATER:").append(getFreeRepeater()); + } + + if(hasPriorityCall()) + { + sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); + sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); + } + sb.append(" ").append(getServiceOptions()); + sb.append(" UNK1:").append(getUnknownField1()); + sb.append(" UNK2:").append(getUnknownField2()); + return sb.toString(); + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraTerminator.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraTerminator.java new file mode 100644 index 00000000..ee828325 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraTerminator.java @@ -0,0 +1,106 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera; + +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.identifier.DMRTalkgroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera Terminator + */ +public class HyteraTerminator extends HyteraFullLC +{ + private TalkgroupIdentifier mTalkgroup; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public HyteraTerminator(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC HYTERA TERMINATOR USER FM:"); + sb.append(getSourceRadio()); + sb.append(" TO:").append(getTalkgroup()); + if(isAllChannelsBusy()) + { + sb.append(" ALL REPEATERS BUSY"); + } + else + { + sb.append(" FREE REPEATER:").append(getFreeRepeater()); + } + + if(hasPriorityCall()) + { + sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); + sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); + } + sb.append(" ").append(getServiceOptions()); + sb.append(" UNK1:").append(getUnknownField1()); + sb.append(" UNK2:").append(getUnknownField2()); + return sb.toString(); + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraUnitToUnitVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraUnitToUnitVoiceChannelUser.java new file mode 100644 index 00000000..c5ebc8af --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/hytera/HyteraUnitToUnitVoiceChannelUser.java @@ -0,0 +1,106 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera Individual Voice Channel User + */ +public class HyteraUnitToUnitVoiceChannelUser extends HyteraFullLC +{ + private RadioIdentifier mTargetRadio; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public HyteraUnitToUnitVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC HYTERA XPT INDIVIDUAL VOICE CHANNEL USER FM:"); + sb.append(getSourceRadio()); + sb.append(" TO:").append(getTargetRadio()); + if(isAllChannelsBusy()) + { + sb.append(" ALL REPEATERS BUSY"); + } + else + { + sb.append(" FREE REPEATER:").append(getFreeRepeater()); + } + + if(hasPriorityCall()) + { + sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); + sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); + } + sb.append(" UNK1:").append(getUnknownField1()); + sb.append(" UNK2:").append(getUnknownField2()); + sb.append(" ").append(getServiceOptions()); + return sb.toString(); + } + + /** + * Target Radio address + */ + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_ADDRESS)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java new file mode 100644 index 00000000..8819b6d9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java @@ -0,0 +1,205 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.channel.DMRLogicalChannel; +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.data.lc.full.AbstractVoiceChannelUser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Capacity Plus - Group Voice Channel User + */ +public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver +{ + private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] VOICE_CHANNEL_REPEATER = new int[]{51, 52, 53, 54}; + private static final int[] VOICE_CHANNEL_TIMESLOT = new int[]{55}; + private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] UNKNOWN = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private RadioIdentifier mRadio; + private TalkgroupIdentifier mTalkgroup; + private DMRLogicalChannel mVoiceChannel; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public CapacityPlusGroupVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER FM:"); + sb.append(getRadio()); + sb.append(" TO:").append(getTalkgroup()); + sb.append(" ON CHANNEL:"); + if(hasVoiceChannel()) + { + sb.append(getVoiceChannel()); + } + else + { + sb.append("--"); + } + + sb.append(" UNK:").append(getUnknown()); + sb.append(" ").append(getServiceOptions()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Unknown 8-bit field + */ + public String getUnknown() + { + return getMessage().getHex(UNKNOWN, 2); + } + + /** + * Logical channel number (ie repeater number). + */ + public DMRLogicalChannel getVoiceChannel() + { + if(mVoiceChannel == null) + { + mVoiceChannel = new DMRLogicalChannel(getVoiceChannelRepeater(), getVoiceChannelTimeslot()); + } + + return mVoiceChannel; + } + + /** + * Rest repeater number + */ + public int getVoiceChannelRepeater() + { + return getMessage().getInt(VOICE_CHANNEL_REPEATER) + 1; + } + + /** + * Rest timeslot + * @return + */ + public int getVoiceChannelTimeslot() + { + return getMessage().getInt(VOICE_CHANNEL_TIMESLOT) + 1; + } + + /** + * Indicates if this message has a voice channel indicated for the call + */ + public boolean hasVoiceChannel() + { + return getVoiceChannelRepeater() != 0; + } + + /** + * Source radio address + */ + public RadioIdentifier getRadio() + { + if(mRadio == null) + { + mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mRadio; + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + + if(hasVoiceChannel()) + { + mIdentifiers.add(getVoiceChannel()); + } + } + + return mIdentifiers; + } + + /** + * Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message. + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + return getVoiceChannel().getLSNArray(); + } + + /** + * Applies the LSN to frequency map to the rest channel. + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(getVoiceChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + getVoiceChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java new file mode 100644 index 00000000..c7a9ec25 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java @@ -0,0 +1,210 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.channel.DMRLogicalChannel; +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.data.lc.full.AbstractVoiceChannelUser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Capacity Plus - Wide Area (Multi-Site) Voice Channel User + */ +public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver +{ + private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] REST_REPEATER = new int[]{51, 52, 53, 54}; + private static final int[] REST_TIMESLOT = new int[]{55}; + private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; + private static final int[] UNKNOWN_2 = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; + + private RadioIdentifier mRadio; + private TalkgroupIdentifier mTalkgroup; + private DMRLogicalChannel mRestChannel; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + */ + public CapacityPlusWideAreaVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("FLC MOTOROLA CAP+ WIDE-AREA VOICE CHANNEL USER FM:"); + sb.append(getRadio()); + sb.append(" TO:").append(getTalkgroup()); + sb.append(" REST:"); + if(hasRestChannel()) + { + sb.append(getRestChannel()); + } + else + { + sb.append("--"); + } + sb.append(" UNK1:").append(getUnknown1()); + sb.append(" UNK2:").append(getUnknown2()); + sb.append(" ").append(getServiceOptions()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Unknown1 8-bit field + */ + public int getUnknown1() + { + return getMessage().getInt(UNKNOWN_1); + } + + /** + * Unknown2 8-bit field + */ + public int getUnknown2() + { + return getMessage().getInt(UNKNOWN_2); + } + + /** + * Logical channel number (ie repeater number). + */ + public DMRLogicalChannel getRestChannel() + { + if(mRestChannel == null) + { + mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); + } + + return mRestChannel; + } + + /** + * Rest channel timeslot + */ + public int getRestTimeslot() + { + return getMessage().getInt(REST_TIMESLOT) + 1; + } + + /** + * Rest channel repeater number + */ + public int getRestRepeater() + { + return getMessage().getInt(REST_REPEATER) + 1; + } + + /** + * Indicates if this message has a reset channel defined. + */ + public boolean hasRestChannel() + { + return getRestRepeater() != 0; + } + + /** + * Source radio address + */ + public RadioIdentifier getRadio() + { + if(mRadio == null) + { + mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); + } + + return mRadio; + } + + /** + * Talkgroup address + */ + public TalkgroupIdentifier getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS)); + } + + return mTalkgroup; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + } + + return mIdentifiers; + } + + + /** + * Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message. + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + return getRestChannel().getLSNArray(); + } + + /** + * Applies the LSN to frequency map to the rest channel. + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) + { + if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + getRestChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ActivityUpdateMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ActivityUpdateMessage.java new file mode 100644 index 00000000..1c1763c4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ActivityUpdateMessage.java @@ -0,0 +1,106 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.type.Activity; + +import java.util.Collections; +import java.util.List; + +/** + * Short Link Control Message - Activity Update + */ +public class ActivityUpdateMessage extends ShortLCMessage +{ + private static final int[] TIMESLOT_1_ACTIVITY = new int[]{4, 5, 6, 7}; + private static final int[] TIMESLOT_2_ACTIVITY = new int[]{8, 9, 10, 11}; + private static final int[] TIMESLOT_1_HASH_ADDRESS = new int[]{12, 13, 14, 15, 16, 17, 18, 19}; + private static final int[] TIMESLOT_2_HASH_ADDRESS = new int[]{20, 21, 22, 23, 24, 25, 26, 27}; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public ActivityUpdateMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + + sb.append("SLC ACTIVITY UPDATE TS1 ["); + sb.append(getHashAddressTS1()).append("] "); + sb.append(getActivityTS1()); + sb.append(" / TS2 ["); + sb.append(getHashAddressTS2()).append("] "); + sb.append(getActivityTS2()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Description of current activity for timeslot 1 + */ + public Activity getActivityTS1() + { + return Activity.fromValue(getMessage().getInt(TIMESLOT_1_ACTIVITY)); + } + + /** + * Description of current activity for timeslot 2 + */ + public Activity getActivityTS2() + { + return Activity.fromValue(getMessage().getInt(TIMESLOT_2_ACTIVITY)); + } + + /** + * Hashed address for the radio or talkgroup active on timeslot 1 + */ + public String getHashAddressTS1() + { + return getMessage().getHex(TIMESLOT_1_HASH_ADDRESS, 2).toUpperCase(); + } + + /** + * Hashed address for the radio or talkgroup active on timeslot 2 + */ + public String getHashAddressTS2() + { + return getMessage().getHex(TIMESLOT_2_HASH_ADDRESS, 2).toUpperCase(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/CapacityPlusRestChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/CapacityPlusRestChannel.java new file mode 100644 index 00000000..c83f365a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/CapacityPlusRestChannel.java @@ -0,0 +1,150 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; +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.DMRSite; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Capacity Plus - Rest Channel Notification + */ +public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] UNKNOWN = new int[]{12, 13, 14}; + private static final int[] REST_REPEATER = new int[]{15, 16, 17, 18}; + private static final int[] REST_TIMESLOT = new int[]{19}; + private static final int[] SITE = new int[]{20, 21, 22, 23, 24}; + private static final int[] UNKNOWN_2 = new int[]{25, 26, 27}; + + private DMRLogicalChannel mRestChannel; + private DMRSite mSite; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public CapacityPlusRestChannel(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC MOTOROLA CAP+ SITE:").append(getSite()); + sb.append(" REST CHANNEL:").append(getRestChannel()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Site number + */ + public DMRSite getSite() + { + if(mSite == null) + { + mSite = DMRSite.create(getMessage().getInt(SITE)); + } + + return mSite; + } + + /** + * Rest Channel Number + */ + public DMRLogicalChannel getRestChannel() + { + if(mRestChannel == null) + { + mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); + } + + return mRestChannel; + } + + /** + * Rest repeater + */ + public int getRestRepeater() + { + return getMessage().getInt(REST_REPEATER) + 1; + } + + /** + * Rest timeslot + */ + public int getRestTimeslot() + { + return getMessage().getInt(REST_TIMESLOT) + 1; + } + + /** + * Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message. + */ + @Override + public int[] getLogicalTimeslotNumbers() + { + return getRestChannel().getLSNArray(); + } + + /** + * Applies the LSN to frequency map to the rest channel. + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) + { + if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber()) + { + getRestChannel().setTimeslotFrequency(timeslotFrequency); + } + } + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSite()); + mIdentifiers.add(getRestChannel()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusControlChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusControlChannel.java new file mode 100644 index 00000000..3794f389 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusControlChannel.java @@ -0,0 +1,103 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork; +import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Connect Plus - Control Channel Information + */ +public class ConnectPlusControlChannel extends ShortLCMessage +{ + private static final int[] NETWORK = new int[]{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] SITE = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + + private DMRNetwork mNetwork; + private DMRSite mSite; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public ConnectPlusControlChannel(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC MOTOROLA CON+ CONTROL CHANNEL NETWORK:").append(getNetwork()); + sb.append(" SITE:").append(getSite()); + return sb.toString(); + } + + /** + * DMR Network Information + */ + public DMRNetwork getNetwork() + { + if(mNetwork == null) + { + mNetwork = DMRNetwork.create(getMessage().getInt(NETWORK)); + } + + return mNetwork; + } + + /** + * DMR Site Information + */ + public DMRSite getSite() + { + if(mSite == null) + { + mSite = DMRSite.create(getMessage().getInt(SITE)); + } + + return mSite; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getNetwork()); + mIdentifiers.add(getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusTrafficChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusTrafficChannel.java new file mode 100644 index 00000000..4c5d0d06 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ConnectPlusTrafficChannel.java @@ -0,0 +1,104 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork; +import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; + +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Connect Plus - Control Channel Information + */ +public class ConnectPlusTrafficChannel extends ShortLCMessage +{ + private static final int[] NETWORK = new int[]{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] SITE = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + + private DMRNetwork mNetwork; + private DMRSite mSite; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public ConnectPlusTrafficChannel(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC MOTOROLA CON+ TRAFFIC CHANNEL NETWORK:").append(getNetwork()); + sb.append(" SITE:").append(getSite()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * DMR Network Information + */ + public DMRNetwork getNetwork() + { + if(mNetwork == null) + { + mNetwork = DMRNetwork.create(getMessage().getInt(NETWORK)); + } + + return mNetwork; + } + + /** + * DMR Site Information + */ + public DMRSite getSite() + { + if(mSite == null) + { + mSite = DMRSite.create(getMessage().getInt(SITE)); + } + + return mSite; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getNetwork()); + mIdentifiers.add(getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ControlChannelSystemParameters.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ControlChannelSystemParameters.java new file mode 100644 index 00000000..26f93b7e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ControlChannelSystemParameters.java @@ -0,0 +1,112 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Tier III - Control Channel System Parameters + */ +public class ControlChannelSystemParameters extends ShortLCMessage +{ + private static final int SYSTEM_IDENTITY_CODE_OFFSET = 4; + private static final int REGISTRATION_REQUIRED_FLAG = 18; + private static final int[] COMMON_SLOT_COUNTER = new int[]{19, 20, 21, 22, 23, 24, 25, 26, 27}; + + private SystemIdentityCode mSystemIdentityCode; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param message containing link control data + */ + public ControlChannelSystemParameters(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC TIER III CONTROL CHANNEL ").append(getSystemIdentityCode().getModel()); + sb.append(" NET:").append(getSystemIdentityCode().getNetwork()); + sb.append(" SITE:").append(getSystemIdentityCode().getSite()); + if(isRegistrationRequired()) + { + sb.append(" REGISTRATION REQUIRED"); + } + sb.append(" SLOT COUNTER:").append(getCommonSlotCounter()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * System Identity Code structure + */ + public SystemIdentityCode getSystemIdentityCode() + { + if(mSystemIdentityCode == null) + { + mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, false); + } + + return mSystemIdentityCode; + } + + /** + * Indicates if registration is required to access this network + */ + public boolean isRegistrationRequired() + { + return getMessage().get(REGISTRATION_REQUIRED_FLAG); + } + + /** + * Common Slot Counter + */ + public int getCommonSlotCounter() + { + return getMessage().getInt(COMMON_SLOT_COUNTER); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/HyteraXPTChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/HyteraXPTChannel.java new file mode 100644 index 00000000..6cd608f6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/HyteraXPTChannel.java @@ -0,0 +1,123 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; + +/** + * Hytera Channel Information + */ +public class HyteraXPTChannel extends ShortLCMessage +{ + private static final int[] FREE_REPEATER = new int[]{12, 13, 14, 15}; + private static final int[] PRIORITY_CALL_REPEATER = new int[]{16, 17, 18, 19}; + private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{20, 21, 22, 23, 24, 25, 26, 27}; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public HyteraXPTChannel(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + + sb.append("SLC HYTERA XPT"); + if(isAllChannelsBusy()) + { + sb.append(" ALL REPEATERS BUSY"); + } + else + { + sb.append(" FREE REPEATER:").append(getFreeRepeater()); + } + + if(hasPriorityCall()) + { + sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); + sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Indicates if all free repreaters are busy + */ + public boolean isAllChannelsBusy() + { + return getFreeRepeater() == 0; + } + + /** + * Next available free repeater + */ + public int getFreeRepeater() + { + return getMessage().getInt(FREE_REPEATER); + } + + /** + * Indicates if there is a priority call on another repeater channel that users should monitor when their + * talkgroup matches the priority call hashed address + */ + public boolean hasPriorityCall() + { + return getPriorityCallRepeater() > 0; + } + + /** + * Priority call repeater number + */ + public int getPriorityCallRepeater() + { + return getMessage().getInt(PRIORITY_CALL_REPEATER); + } + + /** + * Hashed address for priority call + */ + public String getPriorityCallHashedAddress() + { + return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)); + } + + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/NullMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/NullMessage.java new file mode 100644 index 00000000..8a18ce85 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/NullMessage.java @@ -0,0 +1,61 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; + +/** + * Null Short Link Control Message + */ +public class NullMessage extends ShortLCMessage +{ + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public NullMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC IDLE/NULL MESSAGE"); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/SLCAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/SLCAssembler.java new file mode 100644 index 00000000..0bb70579 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/SLCAssembler.java @@ -0,0 +1,248 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.Hamming17; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory; +import io.github.dsheirer.module.decode.dmr.message.type.LCSS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Reassembles fragments into Short Link Control Messages + */ +public class SLCAssembler +{ + private final static Logger mLog = LoggerFactory.getLogger(SLCAssembler.class); + private CorrectedBinaryMessage mAssemblingMessage; + private int mFragmentCount = 0; + private long mTimestamp; + + /** + * Constructs an instance + */ + public SLCAssembler() + { + } + + /** + * Processes the short link control fragment + * @return a short link control message or null if the message is not yet fully assembled + */ + public ShortLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp) + { + mTimestamp = timestamp; + + ShortLCMessage message = null; + + switch(lcss) + { + case FIRST_FRAGMENT: + message = dispatch(); + mAssemblingMessage = new CorrectedBinaryMessage(68); + add(fragment); + break; + case CONTINUATION_FRAGMENT: + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(68); + mAssemblingMessage.setPointer(17); + } + add(fragment); + break; + case LAST_FRAGMENT: + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(68); + mAssemblingMessage.setPointer(51); + } + add(fragment); + message = dispatch(); + break; + case SINGLE_FRAGMENT: + default: + message = createSingleFragment(fragment); + break; + } + + return message; + } + + /** + * Creates a single fragment short link control message + * @param binaryMessage from cach payload + * @return short link control message + */ + private ShortLCMessage createSingleFragment(BinaryMessage binaryMessage) + { + CorrectedBinaryMessage corrected = new CorrectedBinaryMessage(17); + for(int x = 0; x < 17; x++) + { + if(binaryMessage.get(x)) + { + corrected.set(x); + } + } + + return LCMessageFactory.createShort(corrected, mTimestamp, 0); + } + + private ShortLCMessage dispatch() + { + ShortLCMessage message = null; + + if(mFragmentCount == 4) + { + message = decode(mAssemblingMessage, mTimestamp); + } + + mAssemblingMessage = null; + mFragmentCount = 0; + return message; + } + + private static ShortLCMessage decode(BinaryMessage interleaved, long timestamp) + { + if(interleaved != null) + { + boolean valid = true; + + CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(68); + int i, src; + + for(int index = 0; index < 67; index++) + { + if(interleaved.get(index)) + { + deinterleaved.set((index * 17 % 67)); + } + } + + if(interleaved.get(67)) + { + deinterleaved.set(67); + } + + int errorCount = 0; + + for(int row = 0; row < 4; row++) + { + int rowErrorCount = Hamming17.checkAndCorrect(deinterleaved, row * 17); + errorCount += rowErrorCount; + + if(rowErrorCount > 1) + { + valid = false; + } + } + + //If valid, check the column parity values + if(valid) + { + //Check the column parity values + for(int column = 0; column < 17; column++) + { + boolean parity = deinterleaved.get(column); + + for(int y = 1; y < 4; y++) + { + parity ^= deinterleaved.get(column + (y * 17)); + } + + if(parity) + { + valid = false; + } + } + } + + //Extract the message + CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(51); + + for(int row = 0; row < 3; row++) + { + for(int column = 0; column < 12; column++) + { + extractedMessage.set(row * 12 + column, deinterleaved.get(row * 17 + column)); + } + } + + extractedMessage.setCorrectedBitCount(errorCount); + extractedMessage.setSize(36); + ShortLCMessage slco = LCMessageFactory.createShort(extractedMessage, timestamp, 0); + + + //Note: slco is timeslot-agnostic, so we use timeslot 0 every time +// ShortLCMessage slco = LCMessageFactory.createShort(decoded, timestamp, 0); + slco.setValid(valid); + return slco; + } + + return null; + } + + /** + * Adds the cach payload to the currently assembling message + */ + private void add(BinaryMessage payload) + { + if(payload == null || payload.size() != 17) + { + return; + } + + mFragmentCount++; + + try + { + if(mAssemblingMessage == null) + { + mAssemblingMessage = new CorrectedBinaryMessage(68); + } + + for(int x = 0; x < 17; x++) + { + mAssemblingMessage.add(payload.get(x)); + } + } + catch(BitSetFullException bsfe) + { + //Something went wrong and we overfilled the bitset + dispatch(); + } + } + + public static void main(String[] args) + { + String raw = "A0000C0000A0000C0000"; + System.out.println(" RAW:" + raw); + String bits = "10100000000000000110000000000000001010000000000000011000000000000000"; + + BinaryMessage rawBin= BinaryMessage.load(bits); + System.out.println("ORIG:" + rawBin.toHexString()); + + ShortLCMessage slc = decode(rawBin, System.currentTimeMillis()); + + System.out.println(slc.toString()); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java new file mode 100644 index 00000000..b9fa8ca8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java @@ -0,0 +1,104 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +/** + * Base short link control message + */ +public abstract class ShortLCMessage extends LCMessage implements IMessage +{ + protected static final int VENDOR_FLAG = 0; + protected static final int[] OPCODE = new int[]{0, 1, 2, 3}; + + //Note: it is possible that vendor (ie feature ID) is present when the opcode is >= 8, meaning bit 0 is the vendor flag. + protected static final int[] VENDOR = new int[]{4, 5, 6, 7, 8, 9, 10, 11}; + + /** + * Constructs an instance + * + * @param message containing the short link control message bits + */ + public ShortLCMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + /** + * Opcode for this message + */ + @Override + public LCOpcode getOpcode() + { + return getOpcode(getMessage()); + } + + /** + * Opcode numeric value + */ + protected int getOpcodeValue() + { + return getMessage().getInt(OPCODE); + } + + /** + * Vendor for this message + */ + @Override + public Vendor getVendor() + { + return getVendor(getMessage()); + } + + /** + * Lookup the short link control opcode for the specified binary message + */ + public static LCOpcode getOpcode(BinaryMessage binaryMessage) + { + Vendor vendor = getVendor(binaryMessage); + return LCOpcode.fromValue(false, binaryMessage.getInt(OPCODE), vendor); + } + + /** + * Lookup the short link control vendor for the specified binary message + */ + public static Vendor getVendor(BinaryMessage binaryMessage) + { + Vendor vendor = null; + + if(binaryMessage.get(VENDOR_FLAG)) + { + vendor = Vendor.fromValue(binaryMessage.getInt(VENDOR)); + } + + if(vendor == null || vendor == Vendor.UNKNOWN) + { + vendor = Vendor.STANDARD; + } + + return vendor; + } +} + diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/UnknownShortLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/UnknownShortLCMessage.java new file mode 100644 index 00000000..06e800e9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/UnknownShortLCMessage.java @@ -0,0 +1,58 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; + +public class UnknownShortLCMessage extends ShortLCMessage +{ + /** + * Constructs an instance + * + * @param message containing link control data + */ + public UnknownShortLCMessage(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC ERROR] "); + } + sb.append("SLC UNKNOWN OPCODE:").append(getOpcodeValue()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCAssembler.java new file mode 100644 index 00000000..353a49ed --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCAssembler.java @@ -0,0 +1,134 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.mbc; + +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; + +import java.util.ArrayList; +import java.util.List; + +/** + * Assembles a Multi-Block CSBK message into a MultiCSBK message + */ +public class MBCAssembler +{ + private MBCHeader mTS0Header; + private MBCHeader mTS1Header; + private List mTS0ContinuationBlocks = new ArrayList<>(); + private List mTS1ContinuationBlocks = new ArrayList<>(); + + /** + * Constructs an instance + */ + public MBCAssembler() + { + } + + /** + * Processes the MBC header + * @param header to process + */ + public void process(MBCHeader header) + { + if(header != null && header.isValid()) + { + reset(header.getTimeslot()); + + if(header.getTimeslot() == 0) + { + mTS0Header = header; + } + else + { + mTS1Header = header; + } + } + } + + /** + * Processes the MBC continuation block + * @param continuationBlock to process + * @return assembled multi-block CSBK if this is the final block, null otherwise + */ + public CSBKMessage process(MBCContinuationBlock continuationBlock) + { + if(continuationBlock != null) + { + if(continuationBlock.getTimeslot() == 0) + { + mTS0ContinuationBlocks.add(continuationBlock); + + if(continuationBlock.isLastBlock()) + { + if(mTS0Header != null) + { + CSBKMessage csbk = CSBKMessageFactory.create(mTS0Header, new ArrayList<>(mTS0ContinuationBlocks)); + reset(0); + return csbk; + } + else + { + reset(0); + } + } + } + else + { + mTS1ContinuationBlocks.add(continuationBlock); + + if(continuationBlock.isLastBlock()) + { + if(mTS1Header != null) + { + CSBKMessage csbk = CSBKMessageFactory.create(mTS1Header, new ArrayList<>(mTS1ContinuationBlocks)); + reset(1); + return csbk; + } + else + { + reset(1); + } + } + } + } + + return null; + } + + /** + * Resets the specified timeslot message sequence + * @param timeslot to reset + */ + public void reset(int timeslot) + { + if(timeslot == 0) + { + mTS0Header = null; + mTS0ContinuationBlocks.clear(); + } + else + { + mTS1Header = null; + mTS1ContinuationBlocks.clear(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCContinuationBlock.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCContinuationBlock.java new file mode 100644 index 00000000..b92c93c4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MBCContinuationBlock.java @@ -0,0 +1,79 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2020 Zhenyu Mao + * + * This 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 + * ***************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.data.mbc; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessage; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +import java.util.Collections; +import java.util.List; + +public class MBCContinuationBlock extends DataMessage +{ + private static final int LAST_BLOCK = 0; + + /** + * DMR message frame. This message is comprised of a 24-bit prefix and a 264-bit message frame. Outbound base + * station frames transmit a Common Announcement Channel (CACH) in the 24-bit prefix, whereas Mobile inbound frames + * do not use the 24-bit prefix. + * + * @param syncPattern + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + */ + public MBCContinuationBlock(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(); + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" MULTI-BLOCK CSBK CONTINUATION FINAL BLOCK:").append(isLastBlock() ? "YES" : "NO"); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Indicates if this is the last/final block of a multi-block message fragment sequence. + */ + public boolean isLastBlock() + { + return getMessage().get(LAST_BLOCK); + } + + @Override + public boolean isValid() + { + return true; + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MultiCSBK.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MultiCSBK.java new file mode 100644 index 00000000..76680ba2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/MultiCSBK.java @@ -0,0 +1,56 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.mbc; + +import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; +import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; + +import java.util.ArrayList; +import java.util.List; + +/** + * Reassembled Multi-Block CSBK Message + */ +public abstract class MultiCSBK extends CSBKMessage +{ + protected List mBlocks; + + /** + * Constructs an instance + * + * @param header for the MBC sequence + * @param continuationBlocks for the message + */ + public MultiCSBK(MBCHeader header, List continuationBlocks) + { + super(header.getSyncPattern(), header.getMessage(), header.getCACH(), header.getSlotType(), + header.getTimestamp(), header.getTimeslot()); + + mBlocks = continuationBlocks; + + if(mBlocks == null) + { + mBlocks = new ArrayList<>(); + } + + //TODO: figure out how to determine validity + setValid(true); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/UnknownMultiCSBK.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/UnknownMultiCSBK.java new file mode 100644 index 00000000..4a16c11a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/mbc/UnknownMultiCSBK.java @@ -0,0 +1,96 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.mbc; + +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode; +import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; +import io.github.dsheirer.module.decode.dmr.message.type.Vendor; + +import java.util.Collections; +import java.util.List; + +/** + * Unknown Multi-Block CSBK Message + */ +public class UnknownMultiCSBK extends MultiCSBK +{ + /** + * Constructs an instance + * + * @param header + * @param continuationBlocks of the message + */ + public UnknownMultiCSBK(MBCHeader header, List continuationBlocks) + { + super(header, continuationBlocks); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" CSBK *UNKNOWN*"); + + Vendor vendor = getVendor(); + + if(vendor == Vendor.UNKNOWN) + { + sb.append(" VENDOR:").append(getVendorID()); + } + else if(vendor != Vendor.STANDARD) + { + sb.append(" ").append(vendor); + } + + Opcode opcode = getOpcode(); + + if(opcode != Opcode.UNKNOWN) + { + sb.append(" ").append(opcode); + } + else + { + sb.append(" UNKNOWN OPCODE:").append(getOpcodeValue()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + + for(MBCContinuationBlock block: mBlocks) + { + sb.append(block.getMessage().toHexString()); + } + + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/DMRPacketMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/DMRPacketMessage.java new file mode 100644 index 00000000..063295a9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/DMRPacketMessage.java @@ -0,0 +1,99 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.packet; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.message.DMRMessage; +import io.github.dsheirer.module.decode.ip.IPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Packet message assembled from a packet sequence and decoded into an IPacket implementation + */ +public class DMRPacketMessage extends DMRMessage +{ + private PacketSequence mPacketSequence; + private IPacket mPacket; + private List mIdentifiers; + + /** + * Constructs an instance + * @param packetSequence that was transmitted + * @param packet implementation + * @param rawPacket that was extracted from the packet sequence + * @param timeslot for the packet + * @param timestamp for the initial preamble or packet header + */ + public DMRPacketMessage(PacketSequence packetSequence, IPacket packet, CorrectedBinaryMessage rawPacket, + int timeslot, long timestamp) + { + super(rawPacket, timestamp, timeslot); + mPacketSequence = packetSequence; + mPacket = packet; + } + + /** + * Packet sequence containing preamble(s), headers, and data blocks. + */ + public PacketSequence getPacketSequence() + { + return mPacketSequence; + } + + /** + * Packet implementation + */ + public IPacket getPacket() + { + return mPacket; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("CC:").append(getPacketSequence().getPacketSequenceHeader().getSlotType().getColorCode()); + sb.append(" FM:").append(getPacketSequence().getPacketSequenceHeader().getSourceLLID()); + sb.append(" TO:").append(getPacketSequence().getPacketSequenceHeader().getDestinationLLID()); + sb.append(" ").append(getPacket().toString()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(getPacketSequence().hasPacketSequenceHeader()) + { + mIdentifiers.addAll(getPacketSequence().getPacketSequenceHeader().getIdentifiers()); + } + + mIdentifiers.addAll(getPacket().getIdentifiers()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java new file mode 100644 index 00000000..92234576 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java @@ -0,0 +1,172 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.packet; + +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble; +import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sequence of DMR packets that contain a header and one or more data blocks. + */ +public class PacketSequence +{ + private int mTimeslot; + private PacketSequenceHeader mPacketSequenceHeader; + private ProprietaryDataHeader mProprietaryDataHeader; + private List mPreambles = new ArrayList<>(); + private List mDataBlocks = new ArrayList<>(); + + public PacketSequence(int timeslot) + { + mTimeslot = timeslot; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("PACKET SEQUENCE "); + sb.append(" TS:").append(getTimeslot()); + if(mPacketSequenceHeader != null) + { + sb.append(mPacketSequenceHeader.toString()); + } + sb.append(" HEADER:").append(hasPacketSequenceHeader()); + sb.append(" PROPRIETARY HEADER:").append(hasProprietaryDataHeader()); + sb.append(" DATA BLOCKS:").append(mDataBlocks.size()); + sb.append(" PREAMBLES:").append(mPreambles.size()); + return sb.toString(); + } + + /** + * Indicates if this packet sequence is complete with a primary header, an optional proprietary header and data + * blocks where the count of data blocks and proprietary header equals the blocks to follow count from the + * primary header. + */ + public boolean isComplete() + { + int count = 0; + + if(hasProprietaryDataHeader()) + { + count++; + } + + count += mDataBlocks.size(); + + if(hasPacketSequenceHeader()) + { + return getPacketSequenceHeader().getBlocksToFollow() == count; + } + + return false; + } + + public int getTimeslot() + { + return mTimeslot; + } + + /** + * Packet sequence header + */ + public void setPacketSequenceHeader(PacketSequenceHeader header) + { + mPacketSequenceHeader = header; + } + + /** + * Proprietary data header + */ + public void setProprietaryHeader(ProprietaryDataHeader header) + { + mProprietaryDataHeader = header; + } + + + /** + * Adds a preamble to this packet sequence + */ + public void addPreamble(Preamble preamble) + { + mPreambles.add(preamble); + } + + /** + * Adds a data block to this sequence + */ + public void addDataBlock(DataBlock dataBlock) + { + mDataBlocks.add(dataBlock); + } + + /** + * Primary packet sequence header + */ + public PacketSequenceHeader getPacketSequenceHeader() + { + return mPacketSequenceHeader; + } + + /** + * Indicates if this packet sequence has a primary header + */ + public boolean hasPacketSequenceHeader() + { + return mPacketSequenceHeader != null; + } + + /** + * Secondary/Proprietary packet sequence header + */ + public ProprietaryDataHeader getProprietaryDataHeader() + { + return mProprietaryDataHeader; + } + + /** + * Indicates if this packet sequence has a secondary proprietary header + */ + public boolean hasProprietaryDataHeader() + { + return mProprietaryDataHeader != null; + } + + /** + * Data blocks for this packet sequence + */ + public List getDataBlocks() + { + return mDataBlocks; + } + + /** + * Indicates if this packet sequence has captured any data blocks. + */ + public boolean hasDataBlocks() + { + return !mDataBlocks.isEmpty(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java new file mode 100644 index 00000000..cc0d2491 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java @@ -0,0 +1,191 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.packet; + +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble; +import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; +import io.github.dsheirer.sample.Listener; + +/** + * Reassembles packet sequences + */ +public class PacketSequenceAssembler +{ + private Listener mMessageListener; + private PacketSequence mTimeslot0Sequence; + private PacketSequence mTimeslot1Sequence; + + /** + * Constructs an instance + */ + public PacketSequenceAssembler() + { + } + + /** + * Gets the current packet sequence for the specified timeslot, constructing a new sequence as necessary. + * @param timeslot of the packet sequence + * @return timeslot sequence + */ + private PacketSequence getPacketSequence(int timeslot) + { + if(timeslot == 0) + { + if(mTimeslot0Sequence == null) + { + mTimeslot0Sequence = new PacketSequence(1); + } + + return mTimeslot0Sequence; + } + else + { + if(mTimeslot1Sequence == null) + { + mTimeslot1Sequence = new PacketSequence(2); + } + + return mTimeslot1Sequence; + } + } + + /** + * Sets the listener to receive packet packet sequence messages + * @param listener to receive messages + */ + public void setMessageListener(Listener listener) + { + mMessageListener = listener; + } + + /** + * Dispatches the packet sequence for the specified timeslot and sets the sequence to null. + * @param timeslot to dispatch + */ + private void dispatchPacketSequence(int timeslot) + { + PacketSequence packetSequence = (timeslot == 0 ? mTimeslot0Sequence : mTimeslot1Sequence); + + if(mMessageListener != null && packetSequence != null && packetSequence.isComplete()) + { + IMessage message = PacketSequenceMessageFactory.create(packetSequence); + + if(message != null) + { + mMessageListener.receive(message); + } + } + + if(timeslot == 0) + { + mTimeslot0Sequence = null; + } + else + { + mTimeslot1Sequence = null; + } + } + + /** + * Processes a packet sequence preamble. + * + * Note: DMR systems can transmit several preamble messages prior to the actual packet sequence. + * @param preamble for a packet sequence + */ + public void process(Preamble preamble) + { + int timeslot = preamble.getTimeslot(); + + PacketSequence packetSequence = getPacketSequence(timeslot); + + //If we already have headers or data blocks for the current sequence, then this is a new sequence + if(packetSequence.hasPacketSequenceHeader() || + packetSequence.hasProprietaryDataHeader() || + packetSequence.hasDataBlocks()) + { + dispatchPacketSequence(timeslot); + packetSequence = getPacketSequence(timeslot); + } + + packetSequence.addPreamble(preamble); + } + + /** + * Processes a packet sequence header message. + * + * Note: a DMR packet sequence will have at least one header, but can also have a second header. + * @param header to process + */ + public void process(PacketSequenceHeader header) + { + int timeslot = header.getTimeslot(); + + PacketSequence packetSequence = getPacketSequence(timeslot); + + //If we already have headers or data blocks for the current sequence, then this is a new sequence + if(packetSequence.hasPacketSequenceHeader() || + packetSequence.hasProprietaryDataHeader() || + packetSequence.hasDataBlocks()) + { + dispatchPacketSequence(timeslot); + packetSequence = getPacketSequence(timeslot); + } + + packetSequence.setPacketSequenceHeader(header); + } + + /** + * Processes a proprietary packet sequence header. + */ + public void process(ProprietaryDataHeader proprietaryHeader) + { + int timeslot = proprietaryHeader.getTimeslot(); + + PacketSequence packetSequence = getPacketSequence(timeslot); + + //If we already have headers or data blocks for the current sequence, then this is a new sequence + if(packetSequence.hasProprietaryDataHeader() || packetSequence.hasDataBlocks()) + { + dispatchPacketSequence(timeslot); + packetSequence = getPacketSequence(timeslot); + } + + packetSequence.setProprietaryHeader(proprietaryHeader); + } + + /** + * Processes a data block. + * @param dataBlock to process + */ + public void process(DataBlock dataBlock) + { + int timeslot = dataBlock.getTimeslot(); + PacketSequence packetSequence = getPacketSequence(timeslot); + packetSequence.addDataBlock(dataBlock); + + if(packetSequence.isComplete()) + { + dispatchPacketSequence(timeslot); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java new file mode 100644 index 00000000..6515c4b1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java @@ -0,0 +1,226 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.packet; + +import io.github.dsheirer.bits.BitSetFullException; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock; +import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage; +import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader; +import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType; +import io.github.dsheirer.module.decode.ip.UnknownPacket; +import io.github.dsheirer.module.decode.ip.ars.ARSPacket; +import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header; +import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet; +import io.github.dsheirer.module.decode.ip.lrrp.LRRPPacket; +import io.github.dsheirer.module.decode.ip.xcmp.XCMPPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Converts a DMR packet sequence into a message + */ +public class PacketSequenceMessageFactory +{ + private final static Logger mLog = LoggerFactory.getLogger(PacketSequenceMessageFactory.class); + + /** + * Creates a message from a packet sequence + * @param packetSequence with message parts + * @return message + */ + public static IMessage create(PacketSequence packetSequence) + { + if(packetSequence != null && packetSequence.isComplete()) + { + PacketSequenceHeader primaryHeader = packetSequence.getPacketSequenceHeader(); + CorrectedBinaryMessage packet = getPacket(packetSequence, primaryHeader.isConfirmedData()); + + if(packet != null) + { + switch(primaryHeader.getServiceAccessPoint()) + { + case IP_PACKET_DATA: + return createIPPacketData(packetSequence, packet); + case PROPRIETARY_DATA: + return createProprietary(packetSequence, packet); + case SHORT_DATA: + return createDefinedShortData(packetSequence, packet); + default: + mLog.info("Unknown Packet SAP: " + primaryHeader.getServiceAccessPoint() + " - returning unknown packet"); + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + } + } + + //TODO: NOTE: Don't create a DMRPacketMessage unless we have a)Packet and b)Packet Sequence Header + + return null; + } + + /** + * Creates a proprietary data message from the packet sequence + * @param packetSequence containing proprietary message fragments + * @param packet reassembled from the fragments + * @return message + */ + public static IMessage createProprietary(PacketSequence packetSequence, CorrectedBinaryMessage packet) + { + HeaderMessage secondaryHeader = packetSequence.getProprietaryDataHeader(); + + if(secondaryHeader instanceof MNISProprietaryDataHeader) + { + ApplicationType applicationType = ((MNISProprietaryDataHeader)secondaryHeader).getApplicationType(); + + switch(applicationType) + { + case AUTOMATIC_REGISTRATION_SERVICE: + return new DMRPacketMessage(packetSequence, new ARSPacket(packet, 0), + packet,packetSequence.getTimeslot(), + packetSequence.getPacketSequenceHeader().getTimestamp()); + case MNIS_LRRP: + case LOCATION_REQUEST_RESPONSE_PROTOCOL: + return new DMRPacketMessage(packetSequence, new LRRPPacket(packet, 0), packet, + packetSequence.getTimeslot(), + packetSequence.getPacketSequenceHeader().getTimestamp()); + case EXTENSIBLE_COMMAND_MESSAGE_PROTOCOL: + return new DMRPacketMessage(packetSequence, new XCMPPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + default: + mLog.info("Unknown MNIS Application Type: " + MNISProprietaryDataHeader.getApplicationTypeValue(packet)); + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + } + else if(secondaryHeader instanceof HyteraProprietaryDataHeader) + { + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + else + { + mLog.info("Unknown Proprietary Packet Header Type - creating unknown packet."); + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + } + + /** + * Creates an IP packet from the packet sequence + * @param packetSequence containing IP packet fragments + * @param packet that is reassembled from the fragments + * @return message + */ + public static IMessage createIPPacketData(PacketSequence packetSequence, CorrectedBinaryMessage packet) + { + int version = IPV4Header.getIPVersion(packet, 0); + + switch(version) + { + case 4: + return new DMRPacketMessage(packetSequence, new IPV4Packet(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + default: + mLog.info("Unrecognized IP Packet Version: " + version + " - returning unknown packet"); + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + } + + /** + * Creates a Short Data packet from the packet sequence + * @param packetSequence containin the short data sequence + * @param packet with the reassembled contents + * @return constructed DMR packet sequence message + */ + public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet) + { + mLog.info("Unknown Short Data Packet Header Type - creating unknown packet."); + return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + + /** + * Extracts and reassembles the packet contents from a Confirmed or Unconfirmed packet sequence. + * @param sequence that contains a Header and Data Blocks + * @param confirmed is true or unconfirmed is false + * @return the extracted packet or null. + */ + public static CorrectedBinaryMessage getPacket(PacketSequence sequence, boolean confirmed) + { + List fragments = new ArrayList<>(); + int length = 0; + + if(sequence.hasProprietaryDataHeader()) + { + CorrectedBinaryMessage prefix = ((ProprietaryDataHeader)sequence.getProprietaryDataHeader()).getPacketPrefix(); + + if(prefix != null) + { + fragments.add(prefix); + length += prefix.size(); + } + } + + for(DataBlock dataBlock: sequence.getDataBlocks()) + { + //TODO: check data block sequence numbers for confirmed payloads + CorrectedBinaryMessage fragment = (confirmed ? dataBlock.getConfirmedPayload() : dataBlock.getUnConfirmedPayload()); + fragments.add(fragment); + length += fragment.size(); + } + + if(length > 0) + { + CorrectedBinaryMessage packet = new CorrectedBinaryMessage(length); + + for(CorrectedBinaryMessage fragment: fragments) + { + for(int x = 0; x < fragment.size(); x++) + { + try + { + packet.add(fragment.get(x)); + } + catch(BitSetFullException bsfe) + { + //We should never get here + mLog.error("BitSet full while assembling packet fragments"); + return packet; + } + } + } + +// mLog.info("Packet Bytes:" + packet.size() + " MSG:" + packet.toHexString()); + + return packet; + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java new file mode 100644 index 00000000..d8013377 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java @@ -0,0 +1,48 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.terminator; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; + +/** + * Terminator data message with link control + */ +public class Terminator extends DataMessageWithLinkControl +{ + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public Terminator(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/usb/USBData.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/usb/USBData.java new file mode 100644 index 00000000..b788e1a6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/usb/USBData.java @@ -0,0 +1,127 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.data.usb; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.edac.CRCDMR; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.data.DataMessage; +import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unified Single Block Data + */ +public class USBData extends DataMessage +{ + private static final int[] SERVICE_TYPE = new int[]{0, 1, 2, 3}; + private static final int[] RESPONSE_DELAY = new int[]{4, 5}; + private static final int PAYLOAD_CONTENT_FLAG = 6; + private static final int RESERVED_FLAG = 7; + private static final int PARAMETERS_START = 8; + private static final int[] TARGET_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79}; + + private List mIdentifiers; + private RadioIdentifier mTargetRadio; + + /** + * Constructs an instance. + * + * @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA + * @param message containing extracted 196-bit payload. + * @param cach for the DMR burst + * @param slotType for this data message + * @param timestamp message was received + * @param timeslot for the DMR burst + */ + public USBData(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + + int correctedBitCount = CRCDMR.correctCCITT80(message, 0, 80, 0x3333); + + //Set message valid flag according to the corrected bit count for the CRC protected message + setValid(correctedBitCount < 2); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + sb.append(" USB DATA BLOCK"); + sb.append(" TO:").append(getTargetRadio()); + sb.append(" SERVICE:").append(getServiceType()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Utility method to lookup service type for the specified message + */ + public static ServiceType getServiceType(CorrectedBinaryMessage message) + { + return ServiceType.fromValue(message.getInt(SERVICE_TYPE)); + } + + /** + * Service type for this message + */ + public ServiceType getServiceType() + { + return getServiceType(getMessage()); + } + + public RadioIdentifier getTargetRadio() + { + if(mTargetRadio == null) + { + mTargetRadio = DMRRadio.createTo(getMessage().getInt(TARGET_RADIO)); + } + + return mTargetRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTargetRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbsoluteChannelParameters.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbsoluteChannelParameters.java new file mode 100644 index 00000000..f5053254 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbsoluteChannelParameters.java @@ -0,0 +1,136 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.channel.DMRAbsoluteChannel; + +/** + * Absolute channel frequency structure. This structure is used in Multi-Block CSBK to define channel parameters. + * + * See: TS 102 361-4 Paragraph 7.1.1.1.2 CG_AP + */ +public class AbsoluteChannelParameters extends AbstractStructure +{ + private static final int LAST_BLOCK_FLAG = 0; + private static final int ENCRYPTION_FLAG = 1; + private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7}; + private static final int[] COLOR_CODE = new int[]{12, 13, 14, 15}; + + //Note: the ICD only defines a value of 0 for this field, so it's currently unused. + private static final int[] CDEF_TYPE = new int[]{16, 17, 18, 19}; + private static final int[] LCN = new int[]{22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}; + private static final int[] UPLINK_FREQUENCY_MHZ = new int[]{34, 35, 36, 37, 38, 39, 40, 41, 42, 43}; + private static final int[] UPLINK_FREQUENCY_125_HZ = new int[]{44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56}; + private static final int[] DOWNLINK_FREQUENCY_MHZ = new int[]{57, 58, 59, 60, 61, 62, 63, 64, 65, 66}; + private static final int[] DOWNLINK_FREQUENCY_125_HZ = new int[]{67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + + private DMRAbsoluteChannel mChannel; + private int mTimeslot; + + /** + * Constructs an instance + * + * @param message containing this structure + * @param offset into the message + * @param timeslot for the channel + */ + public AbsoluteChannelParameters(CorrectedBinaryMessage message, int offset, int timeslot) + { + super(message, offset); + mTimeslot = timeslot; + } + + /** + * DMR Channel defined by this structure + * @return channel + */ + public DMRAbsoluteChannel getChannel() + { + if(mChannel == null) + { + mChannel = new DMRAbsoluteChannel(getChannelNumber(), mTimeslot, getDownlinkFrequency(), getUplinkFrequency()); + } + + return mChannel; + } + + /** + * Color code for the channel. Note: default value is 0 if unspecified. + */ + public int getColorCode() + { + return getMessage().getInt(COLOR_CODE, getOffset()); + } + + /** + * Indicates if this is the Last/Final block in a Multi-Block sequence + */ + public boolean isLastBlock() + { + return getMessage().get(LAST_BLOCK_FLAG + getOffset()); + } + + /** + * Indicates if this block is encrypted + */ + public boolean isEncrypted() + { + return getMessage().get(ENCRYPTION_FLAG + getOffset()); + } + + /** + * Opcode - repeated from the first block in the multi-block sequence + */ + public int getOpcodeValue() + { + return getMessage().getInt(OPCODE); + } + + /** + * Logical Channel Number + */ + public int getChannelNumber() + { + return getMessage().getInt(LCN, getOffset()); + } + + /** + * Channel uplink (MS -> Tower) frequency + * @return frequency in Hz + */ + public long getUplinkFrequency() + { + long frequency = (long)(getMessage().getInt(UPLINK_FREQUENCY_MHZ, getOffset()) * 1E6); + frequency += getMessage().getInt(UPLINK_FREQUENCY_125_HZ) * 125; + return frequency; + } + + /** + * Channel downlink (Tower -> MS) frequency + * @return frequency in Hz + */ + public long getDownlinkFrequency() + { + long frequency = (long)(getMessage().getInt(DOWNLINK_FREQUENCY_MHZ, getOffset()) * 1E6); + frequency += getMessage().getInt(DOWNLINK_FREQUENCY_125_HZ) * 125; + return frequency; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbstractStructure.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbstractStructure.java new file mode 100644 index 00000000..e88bc32c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AbstractStructure.java @@ -0,0 +1,58 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Base structure class for a structure contained within a binary message + */ +public abstract class AbstractStructure +{ + private CorrectedBinaryMessage mMessage; + private int mOffset; + + /** + * Constructs an instance + * @param message that contains this structure + * @param offset into the message to the start of this structure + */ + public AbstractStructure(CorrectedBinaryMessage message, int offset) + { + mMessage = message; + mOffset = offset; + } + + /** + * Message that contains this structure + */ + public CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Offset into the message where this structure starts + */ + public int getOffset() + { + return mOffset; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AcknowledgeType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AcknowledgeType.java new file mode 100644 index 00000000..09e2ca74 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AcknowledgeType.java @@ -0,0 +1,48 @@ +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Acknowledge message type enumeration + */ +public enum AcknowledgeType +{ + ACKNOWLEDGE(0, "ACKNOWLEDGED"), + NOT_ACKNOWLEDGE(1, "REJECTED/REFUSED"), + QUEUED(2, "QUEUED"), + WAIT(3, "WAIT"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param value of the entry + * @param label for the entry + */ + AcknowledgeType(int value, String label) + { + mValue = value; + mLabel = label; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the type from a value. + * @param value of the acknowledge type + * @return type or UNKNOWN + */ + public static AcknowledgeType fromValue(int value) + { + if(0 <= value && value <= 3) + { + return AcknowledgeType.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Activity.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Activity.java new file mode 100644 index 00000000..6d19bbc6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Activity.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Activity enumeration + * + * Defined in: TS 102 361-2 7.1.3.2 Activity Update + */ +public enum Activity +{ + IDLE(0, "IDLE"), + RESERVED_1(1, "RESERVED 1"), + GROUP_CSBK(2, "GROUP CSBK"), + INDIVIDUAL_CSBK(3, "INDIV CSBK"), + RESERVED_4(4, "RESERVED 4"), + RESERVED_5(5, "RESERVED 5"), + RESERVED_6(6, "RESERVED 6"), + RESERVED_7(7, "RESERVED 7"), + GROUP_VOICE(8, "GROUP VOICE"), + INDIVIDUAL_VOICE(9, "INDIV VOICE"), + INDIVIDUAL_DATA(10, "INDIV DATA"), + GROUP_DATA(11, "GROUP DATA"), + EMERGENCY_GROUP_VOICE(12, "EMERG GROUP VOICE"), + EMERGENCY_INDIVIDUAL_VOICE(13, "EMERG INDIV VOICE"), + RESERVED_14(14, "RESERVED 14"), + RESERVED_15(15, "RESERVED 15"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param value of the activity item + * @param label pretty string + */ + Activity(int value, String label) + { + mValue = value; + mLabel = label; + } + + /** + * Pretty version of the entry + */ + @Override + public String toString() + { + return mLabel; + } + + /** + * Lookup the activity from a value + * @param value 0-15 + * @return entry or UNKNOWN + */ + public static Activity fromValue(int value) + { + if(0 <= value && value <= 15) + { + return Activity.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java new file mode 100644 index 00000000..8c376efe --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java @@ -0,0 +1,112 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import java.util.Map; +import java.util.TreeMap; + +/** + * DMR Tier III Announcement Message Type + */ +public enum AnnouncementType +{ + ANNOUNCE_OR_WITHDRAW_TSCC(0, "ANNOUNCE/WITHDRAW TSCC"), + CALL_TIMER_PARAMETERS(1, "CALL TIMER PARAMETERS"), + VOTE_NOW_ADVICE(2, "VOTE NOW ADVICE"), + LOCAL_TIME(3, "BROADCAST LOCAL TIME"), + MASS_REGISTRATION(4, "MASS REGISTRATION"), + CHANNEL_FREQUENCY_ANNOUNCEMENT(5, "CHANNEL FREQUENCY"), + ADJACENT_SITE_INFORMATION(6, "NEIGHBOR SITE INFORMATION"), + GENERAL_SITE_INFORMATION(7, "SITE INFORMATION"), + RESERVED_8(8, "RESERVED 8"), + RESERVED_9(9, "RESERVED 8"), + RESERVED_10(10, "RESERVED 10"), + RESERVED_11(11, "RESERVED 11"), + RESERVED_12(12, "RESERVED 12"), + RESERVED_13(13, "RESERVED 13"), + RESERVED_14(14, "RESERVED 14"), + RESERVED_15(15, "RESERVED 15"), + RESERVED_16(16, "RESERVED 16"), + RESERVED_17(17, "RESERVED 17"), + RESERVED_18(18, "RESERVED 18"), + RESERVED_19(19, "RESERVED 19"), + RESERVED_20(20, "RESERVED 20"), + RESERVED_21(21, "RESERVED 21"), + RESERVED_22(22, "RESERVED 22"), + RESERVED_23(23, "RESERVED 23"), + RESERVED_24(24, "RESERVED 24"), + RESERVED_25(25, "RESERVED 25"), + RESERVED_26(26, "RESERVED 26"), + RESERVED_27(27, "RESERVED 27"), + RESERVED_28(28, "RESERVED 28"), + RESERVED_29(29, "RESERVED 29"), + VENDOR_SPECIFIC_30(30, "VENDOR SPECIFIC 30"), + VENDOR_SPECIFIC_31(31, "VENDOR SPECIFIC 30"), + + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + AnnouncementType(int value, String label) + { + mValue = value; + mLabel = label; + } + + /** + * Value for the entry + */ + public int getValue() + { + return mValue; + } + + private static final Map LOOKUP_MAP = new TreeMap<>(); + + static + { + for(AnnouncementType announcementType: AnnouncementType.values()) + { + LOOKUP_MAP.put(announcementType.getValue(), announcementType); + } + } + + /** + * Utility method to lookup the model type from the integer value + * @param value 0-3 + * @return entry or UNKNOWN + */ + public static AnnouncementType fromValue(int value) + { + if(LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ApplicationType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ApplicationType.java new file mode 100644 index 00000000..e62024b7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ApplicationType.java @@ -0,0 +1,73 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Motorola MOTOTRBO Network Interface Service application types + * + * See: https://cwh050.blogspot.com/2019/08/what-does-mnis-do.html + * + * Note: this is a work in progress enumeration + * Missing: Telemetry, External Data, OTAP, XNA, XCMP, ... + */ +public enum ApplicationType +{ + MNIS_LRRP(0x01, "MNIS LRRP"), + LOCATION_REQUEST_RESPONSE_PROTOCOL(0x11, "LRRP"), + AUTOMATIC_REGISTRATION_SERVICE(0x33, "ARS"), + EXTENSIBLE_COMMAND_MESSAGE_PROTOCOL(0x88, "XCMP"), + UNKNOWN(0, "UNKNOWN"); + + private int mValue; + private String mLabel; + + ApplicationType(int value, String label) + { + mValue = value; + mLabel = label; + } + + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the application type from a value. + * @param value to lookup + * @return application type or UNKNOWN. + */ + public static ApplicationType fromValue(int value) + { + switch(value) + { + case 0x01: + return MNIS_LRRP; + case 0x11: + return LOCATION_REQUEST_RESPONSE_PROTOCOL; + case 0x33: + return AUTOMATIC_REGISTRATION_SERVICE; + case 0x88: + return EXTENSIBLE_COMMAND_MESSAGE_PROTOCOL; + default: + return UNKNOWN; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataPacketFormat.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataPacketFormat.java new file mode 100644 index 00000000..ae669ce4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataPacketFormat.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Enumeration of data packet formats + */ +public enum DataPacketFormat +{ + UNIFIED_DATA_TRANSPORT(0, "UNIFIED DATA TRANSPORT"), + RESPONSE_PACKET(1, "RESPONSE PACKET"), + UNCONFIRMED_DATA_PACKET(2, "UNCONFIRMED DATA PACKET"), + CONFIRMED_DATA_PACKET(3, "CONFIRMED DATA PACKET"), + DEFINED_SHORT_DATA(13, "DEFINED SHORT DATA"), + RAW_OR_STATUS_SHORT_DATA(14, "RAW OR STATUS SHORT DATA"), + PROPRIETARY_DATA_PACKET(15, "PROPRIETARY DATA PACKET"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + DataPacketFormat(int value, String label) + { + mValue = value; + mLabel = label; + } + + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the entry from a value. + * @param value for the format + * @return entry or UNKNOWN + */ + public static DataPacketFormat fromValue(int value) + { + switch(value) + { + case 0: + return UNIFIED_DATA_TRANSPORT; + case 1: + return RESPONSE_PACKET; + case 2: + return UNCONFIRMED_DATA_PACKET; + case 3: + return CONFIRMED_DATA_PACKET; + case 13: + return DEFINED_SHORT_DATA; + case 14: + return RAW_OR_STATUS_SHORT_DATA; + case 15: + return PROPRIETARY_DATA_PACKET; + default: + return UNKNOWN; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataType.java new file mode 100644 index 00000000..29eac064 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DataType.java @@ -0,0 +1,128 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Data Type enumeration + */ +public enum DataType +{ + PI_HEADER(0, 288, "ENCRYPTION HEADER"), + VOICE_HEADER(1, 288, "VOICE HEADER"), + TLC(2, 288, "TERMINATOR"), + CSBK(3, 288, "CSBK"), + MBC_HEADER(4, 288, "MBC HEADER"), + MBC_BLOCK(5, 276, "MBC"), + DATA_HEADER(6, 288, "DATA HEADER"), + RATE_1_OF_2_DATA(7, 276, "RATE 1/2 PACKET"), + RATE_3_OF_4_DATA(8, 276, "RATE 3/4 PACKET"), + SLOT_IDLE(9, 276, "IDLE"), + RATE_1_DATA(10, 276, "RATE 1/1 PACKET"), + USB_DATA(11, 276, "UNIFIED SINGLE BLOCK DATA"), + MBC_ENC_HEADER(12, 276, "MBC ENCRYPTED HEADER"), + DATA_ENC_HEADER(13, 276, "DATA ENCRYPTED HEADER"), + CHANNEL_CONTROL_ENC_HEADER(14, 276, "CONTROL CHANNEL ENCRYPTED HEADER"), + RESERVED_15(15, -1, "RESERVED"), + UNKNOWN(-1, -1, "UNKNOWN"); + + private int mValue; + private int mMessageLength; + private String mLabel; + + DataType(int value, int length, String label) + { + mValue = value; + mMessageLength = length; + mLabel = label; + } + + /** + * Data Unit ID value + */ + public int getValue() + { + return mValue; + } + + /** + * Length of the message in bits + */ + public int getMessageLength() + { + return mMessageLength; + } + + /** + * Short display label + */ + public String getLabel() + { + return mLabel; + } + + /** + * Lookup the Data Unit ID from an integer value + */ + public static DataType fromValue(int value) + { + switch(value) + { + case 0: + return PI_HEADER; + case 1: + return VOICE_HEADER; + case 2: + return TLC; + case 3: + return CSBK; + case 4: + return MBC_HEADER; + case 5: + return MBC_BLOCK; + case 6: + return DATA_HEADER; + case 7: + return RATE_1_OF_2_DATA; + case 8: + return RATE_3_OF_4_DATA; + case 9: + return SLOT_IDLE; + case 10: + return RATE_1_DATA; + case 11: + return USB_DATA; + case 12: + return MBC_ENC_HEADER; + case 13: + return DATA_ENC_HEADER; + case 14: + return CHANNEL_CONTROL_ENC_HEADER; + case 15: + return RESERVED_15; + default: + throw new IllegalArgumentException("Slot Type ID must be in range 0 - 15"); + } + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DefinedDataFormat.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DefinedDataFormat.java new file mode 100644 index 00000000..767524b5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/DefinedDataFormat.java @@ -0,0 +1,113 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Payload Formats for Defined Short Data packets + */ +public enum DefinedDataFormat +{ + BINARY, + BCD, + ASCII_7, + ISO_IEC_8859_1, + ISO_IEC_8859_2, + ISO_IEC_8859_3, + ISO_IEC_8859_4, + ISO_IEC_8859_5, + ISO_IEC_8859_6, + ISO_IEC_8859_7, + ISO_IEC_8859_8, + ISO_IEC_8859_9, + ISO_IEC_8859_10, + ISO_IEC_8859_11, + ISO_IEC_8859_12, + ISO_IEC_8859_13, + ISO_IEC_8859_14, + ISO_IEC_8859_15, + ISO_IEC_8859_16, + UNICODE_UTF_8, + UNICODE_UTF_16, + UNICODE_UTF_16_BE, + UNICODE_UTF_16_LE, + UNICODE_UTF_32, + UNICODE_UTF_32_BE, + UNICODE_UTF_32_LE, + UNKNOWN; + + public static DefinedDataFormat fromValue(int value) + { + switch(value) + { + case 0: + return BINARY; + case 1: + return BCD; + case 2: + return ASCII_7; + case 3: + return ISO_IEC_8859_1; + case 4: + return ISO_IEC_8859_2; + case 5: + return ISO_IEC_8859_3; + case 6: + return ISO_IEC_8859_4; + case 7: + return ISO_IEC_8859_5; + case 8: + return ISO_IEC_8859_6; + case 9: + return ISO_IEC_8859_7; + case 10: + return ISO_IEC_8859_8; + case 11: + return ISO_IEC_8859_9; + case 12: + return ISO_IEC_8859_10; + case 13: + return ISO_IEC_8859_11; + case 14: + return ISO_IEC_8859_13; + case 15: + return ISO_IEC_8859_14; + case 16: + return ISO_IEC_8859_15; + case 17: + return ISO_IEC_8859_16; + case 18: + return UNICODE_UTF_8; + case 19: + return UNICODE_UTF_16; + case 20: + return UNICODE_UTF_16_BE; + case 21: + return UNICODE_UTF_16_LE; + case 22: + return UNICODE_UTF_32; + case 23: + return UNICODE_UTF_32_BE; + case 24: + return UNICODE_UTF_32_LE; + default: + return UNKNOWN; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Gateway.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Gateway.java new file mode 100644 index 00000000..b8a17f9a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Gateway.java @@ -0,0 +1,80 @@ +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Gateway Identifiers + * + * See: TS 102 361.4 paragraph A.4 + */ +public enum Gateway +{ + PSTNI(0xFFFEC0, "PSTN SERVICES - ALIGNED TIMING"), + PABXI(0xFFFEC1, "PABX SERVICES - ALIGNED TIMING"), + LINEI(0xFFFEC2, "LINE GATEWAY - ALIGNED TIMING"), + IPI(0xFFFEC3, "IP GATEWAY - ALIGNED TIMING"), + SUPLI(0xFFFEC4, "SUPPLEMENTARY DATA SERVICE"), + SDMI(0xFFFEC5, "UDT SHORT DATA SERVICE"), + REGI(0xFFFEC6, "REGISTRATION SERVICE"), + MSI(0xFFFEC7, "CALL DIVERSION TO MS"), + DIVERTI(0xFFFEC9, "CALL DIVERSION CANCEL"), + TSI(0xFFFECA, "TRUNK SYSTEM CONTROLLER"), + DISPATI(0xFFFECB, "SYSTEM DISPATCHER - ALIGNED TIMING"), + STUNI(0xFFFECC, "STUN/REVIVE MS ID"), + AUTHI(0xFFFECD, "AUTHENTICATION ID"), + GPI(0xFFFECE, "CALL DIVERSION TO TALKGROUP"), + KILLI(0xFFFECF, "KILL MS ID"), + PSTNDI(0xFFFED0, "PSTN SERVICES - OFFSET TIMING"), + PABXDI(0xFFFED1, "PABX SERVICES - OFFSET TIMING"), + LINEDI(0xFFFED2, "LINE GATEWAY - OFFSET TIMING"), + DISPATDI(0xFFFED3, "SYSTEM DISPATCHER - OFFSET TIMING"), + ALLMIS(0xFFFED4, "ALL MS AND TALKGROUPS"), + IPDI(0xFFFED5, "IP GATEWAY - OFFSET TIMING"), + DGNAI(0xFFFED6, "DYNAMIC GROUP NUMBER ASSIGNMENT"), + TATTSI(0xFFFED7, "TALKGROUP SUBSCRIBE/ATTACH SERVICE"), + ALLMSIDL(0xFFFFFD, "ALL SITE MS AS TALKGROUP"), + ALLMSIDZ(0xFFFFFE, "ALL SITE SUBSET MS AS TALKGROUP"), + ALLMSID(0xFFFFFF, "ALL MS AS TALKGROUP"), + UNKNOWN(-1, "UNKNOWN"); + + private int mAddress; + private String mLabel; + + /** + * Constructs an instance + * @param address of the entry + * @param label for the entry + */ + Gateway(int address, String label) + { + mAddress = address; + mLabel = label; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Address of the gateway identifier + */ + public int getAddress() + { + return mAddress; + } + + /** + * Utility method to lookup the gateway from a value. + * @param value of the service type + * @return type or UNKNOWN + */ + public static Gateway fromValue(int value) + { + if(0 <= value && value <= 15) + { + return Gateway.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/LCSS.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/LCSS.java new file mode 100644 index 00000000..da20c175 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/LCSS.java @@ -0,0 +1,71 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +public enum LCSS +{ + /** + * Link Control(LC) Single Fragment -or- CSBK Signalling First Fragment + */ + SINGLE_FRAGMENT("[FL]"), + + /** + * LC First Fragment + */ + FIRST_FRAGMENT("[F-]"), + + /** + * LC or CSBK Last Fragment + */ + LAST_FRAGMENT("[-L]"), + + /** + * LC or CSBK Continuation Fragment + */ + CONTINUATION_FRAGMENT("[--]"), + + /** + * Unknown Fragment + */ + UNKNOWN("[**]"); + + private String mLabel; + + LCSS(String label) + { + mLabel = label; + } + + public static LCSS fromValue(int value) + { + if(0 <= value && value < 4) + { + return LCSS.values()[value]; + } + + return UNKNOWN; + } + + @Override + public String toString() + { + return mLabel; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Model.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Model.java new file mode 100644 index 00000000..b6043485 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Model.java @@ -0,0 +1,47 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Tier III network model enumeration + */ +public enum Model +{ + TINY, + SMALL, + LARGE, + HUGE, + UNKNOWN; + + /** + * Utility method to lookup the model type from the integer value + * @param value 0-3 + * @return entry or UNKNOWN + */ + public static Model fromValue(int value) + { + if(0 <= value && value <= 3) + { + return Model.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PAR.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PAR.java new file mode 100644 index 00000000..caaa687a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PAR.java @@ -0,0 +1,69 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Tier III PAR subfield. Identifies which mobile subscribers can access a site and indicates if the site + * has more than one control channel such that the mobile subscribers are partitioned into two groups, A and B. + */ +public enum PAR +{ + RESERVED("RESERVED"), + CATEGORY_A("MULTIPLE CONTROL CHANNELS - CAT A SUBSCRIBERS"), + CATEGORY_B("MULTIPLE CONTROL CHANNELS - CAT B SUBSCRIBERS"), + CATEGORY_A_AND_B("SINGLE CONTROL CHANNEL - CAT A & B SUBSCRIBERS"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + PAR(String label) + { + mLabel = label; + } + + /** + * Utility method to lookup the model type from the integer value + * @param value 0-3 + * @return entry or UNKNOWN + */ + public static PAR fromValue(int value) + { + if(0 <= value && value <= 3) + { + return PAR.values()[value]; + } + + return UNKNOWN; + } + + /** + * Indicates if the site is setup for multiple control channels. + */ + public boolean isMultipleControlChannels() + { + return this == CATEGORY_A || this == CATEGORY_B; + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PositionError.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PositionError.java new file mode 100644 index 00000000..89f4e42c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/PositionError.java @@ -0,0 +1,72 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Position Error enumeration. + * + * ETSI 102 361-2 7.2.15 Position Error + */ +public enum PositionError +{ + ERROR_0("LESS THAN 2 METERS"), + ERROR_1("LESS THAN 20 METERS"), + ERROR_2("LESS THAN 200 METERS"), + ERROR_3("LESS THAN 2 KILOMETERS"), + ERROR_4("LESS THAN 20 KILOMETERS"), + ERROR_5("LESS THAN 200 KILOMETERS"), + ERROR_6("MORE THAN 200 KILOMETERS"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + /** + * Constructs an instance + * @param label descriptive + */ + PositionError(String label) + { + mLabel = label; + } + + /** + * Descriptive label + */ + @Override + public String toString() + { + return mLabel; + } + + /** + * Lookup the position error from the value + * @param value 0 - 7 + * @return enum entry or UNKNOWN + */ + public static PositionError fromValue(int value) + { + if(0 <= value && value <= 7) + { + return PositionError.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ProtectKind.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ProtectKind.java new file mode 100644 index 00000000..047951a9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ProtectKind.java @@ -0,0 +1,73 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Protect Kind enumeration + * + * Defined in: TS 102 361-4 7.2.21 Protect_Kind + */ +public enum ProtectKind +{ + DISABLE_PTT(0, "DISABLE PTT"), + ENABLE_PTT(1, "ENABLE PTT"), + ILLEGALLY_PARKED(2, "ILLEGALLY PARKED"), + ENABLE_TARGET_ID_PTT(3, "ENABLE TARGET ID PTT ONLY"), + //All other values reserved + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param value of the item + * @param label pretty string + */ + ProtectKind(int value, String label) + { + mValue = value; + mLabel = label; + } + + /** + * Pretty version of the entry + */ + @Override + public String toString() + { + return mLabel; + } + + /** + * Lookup the entry from a value + * @param value 0-3 + * @return entry or UNKNOWN + */ + public static ProtectKind fromValue(int value) + { + if(0 <= value && value <= 3) + { + return ProtectKind.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Reason.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Reason.java new file mode 100644 index 00000000..2fa3e8cb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Reason.java @@ -0,0 +1,109 @@ +package io.github.dsheirer.module.decode.dmr.message.type; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Acknowledge message reasons + * + * Note: each entry is prefixed with MS or TS where: + * MS = Mobile Subscriber + * TS = Trunking System + */ +public enum Reason +{ + MS_SERVICE_NOT_SUPPORTED(0x00, "SERVICE_NOT SUPPORTED"), + MS_LINE_NOT_SUPPORTED(0x11, "LINE NOT SUPPORTED"), + MS_REFUSED_STACK_FULL(0x12, "MS REFUSED - STACK FULL"), + MS_REFUSED_EQUIPMENT_BUSY(0x13, "MS REFUSED - EQUIPMENT BUSY"), + MS_REFUSED_BY_RECIPIENT(0x14, "REFUSED BY RECIPIENT"), + MS_REFUSED_CUSTOM(0x15, "REFUSED-CUSTOM"), + MS_DUPLEX_NOT_SUPPORTED(0x16, "DUPLEX NOT SUPPORTED BY MS"), + MS_REFUSED_REASON_UNKNOWN(0x1F, "REFUSED-REASON UNKNOWN"), + + TS_SERVICE_NOT_SUPPORTED(0x20, "SERVICE NOT SUPPORTED"), + TS_REFUSED_NOT_PERMITTED(0x21, "REFUSED-NOT PERMISSION"), + TS_REFUSED_SERVICE_TEMP_UNAVAILABLE(0x22, "REFUSED-SERVICE TEMPORARY UNAVAILABLE"), + TS_REFUSED_SERVICE_UNAVAILABLE(0x23, "REFUSED-SERVICE UNAVAILABLE"), + TS_REFUSED_CALLED_RADIO_NOT_REGISTERED(0x24, "REFUSED-CALLED RADIO NOT REGISTERED"), + TS_REFUSED_CALLED_RADIO_OFFLINE(0x25, "REFUSED-CALLED RADIO OFFLINE"), + TS_REFUSED_CALLED_RADIO_CALL_DIVERT(0x26, "REFUSED-CALLED RADIO HAS CALL DIVERSION"), + TS_REFUSED_NETWORK_CONGESTION(0x27, "REFUSED-NETWORK CONGESTION"), + TS_REFUSED_NETWORK_NOT_READY(0x28, "REFUSED-NETWORK NOT READY"), + TS_REFUSED_CANNOT_CANCEL_CALL(0x29, "REFUSED-CANNOT CANCEL CALL"), + TS_REGISTRATION_REFUSED(0x2A, "REGISTRATION REFUSED"), + TS_REGISTRATION_DENIED(0x2B, "REGISTRATION DENIED"), + TS_IP_CONNECTION_FAILED(0x2C, "IP CONNECTION FAILED"), + TS_REFUSED_RADIO_NOT_REGISTERED(0x2D, "REFUSED-RADIO NOT REGISTERED"), + TS_CALLED_PARTY_BUSY(0x2E, "CALLED PARTY BUSY"), + TS_CALLED_TALKGROUP_NOT_ALLOWED(0x2F, "CALLED TALKGROUP NOT ALLOWED"), + TS_CRC_ERROR_IN_UDT_UPLOAD(0x30, "CRC ERROR IN UDT UPLOAD"), + TS_REFUSED_DUPLEX_CALL_NETWORK_CONGESTION(0x31, "REFUSED DUPLEX CALL-NETWORK CONGESTION"), + TS_REFUSED_REASON_UNKNOWN(0x3F, "REFUSED-REASON UNKNONW"), + + + MS_MESSAGE_ACCEPTED(0x44, "MESSAGE ACCEPTED"), + MS_CALLBACK(0x45, "CALLBACK"), + MS_ALERTING(0x46, "ALERTING BUT NOT READY"), + MS_ACCEPTED_FOR_POLLING_STATUS_SERVICE(0x47, "ACCEPTED FOR POLLING STATUS SERVICE"), + MS_AUTHENTICATION_RESPONSE(0x48, "AUTHENTICATION RESPONSE"), + + TS_MESSAGE_ACCEPTED(0x60, "MESSAGE ACCEPTED"), + TS_STORE_AND_FORWARD(0x61, "STORE AND FORWARD"), + TS_REGISTRATION_ACCEPTED(0x62, "REGISTRATION ACCEPTED"), + TS_ACCEPTED_FOR_STATUS_POLLING_SERVICE(0x63, "ACCEPTED FOR STATUS POLLING SERVICE"), + TS_AUTHENTICATION_RESPONSE(0x64, "AUTHENTICATION RESPONSE"), + TS_SUBSCRIPTION_SERVICE_REGISTRATION_ACCEPTED(0x65, "SUBSCRIPTION SERVICE REGISTRATION ACCEPTED"), + + TS_QUEUED_FOR_RESOURCE(0xA0, "QUEUED FOR RESOURCE"), + TS_QUEUED_FOR_BUSY_RADIO(0xAa, "QUEUED FOR BUSY RADIO"), + + TS_WAIT(0xE0, "WAIT"), + + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + Reason(int value, String label) + { + mValue = value; + mLabel = label; + } + + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return mLabel; + } + + private static final Map LOOKUP_MAP = new TreeMap<>(); + + static + { + for(Reason reason: Reason.values()) + { + LOOKUP_MAP.put(reason.getValue(), reason); + } + } + + /** + * Utility method to lookup a Reason entry from a value. + * @param value to lookup + * @return enum entry or UNKNOWN + */ + public static Reason fromValue(int value) + { + if(LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceAccessPoint.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceAccessPoint.java new file mode 100644 index 00000000..87b191b8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceAccessPoint.java @@ -0,0 +1,103 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Enumeration of service access point (SAP) + */ +public enum ServiceAccessPoint +{ + UNIFIED_DATA_TRANSPORT("UNIFIED DATA TRANSPORT"), + SAP_1("RESERVED 1"), + TCP_IP_COMPRESSED_HEADER("TCP/IP HEADER COMPRESSION"), + UDP_IP_COMPRESSED_HEADER("UDP/IP HEADER COMPRESSION"), + IP_PACKET_DATA("IP PACKET DATA"), + ARP("ARP"), + SAP_6("RESERVED 6"), + SAP_7("RESERVED 7"), + SAP_8("RESERVED 8"), + PROPRIETARY_DATA("PROPRIETARY DATA"), + SHORT_DATA("SHORT DATA"), + SAP_11("RESERVED 11"), + SAP_12("RESERVED 12"), + SAP_13("RESERVED 13"), + SAP_14("RESERVED 14"), + SAP_15("RESERVED 15"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + ServiceAccessPoint(String label) + { + mLabel = label; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the entry from a value. + * @param value for the format + * @return entry or UNKNOWN + */ + public static ServiceAccessPoint fromValue(int value) + { + switch(value) + { + case 0: + return UNIFIED_DATA_TRANSPORT; + case 1: + return SAP_1; + case 2: + return TCP_IP_COMPRESSED_HEADER; + case 3: + return UDP_IP_COMPRESSED_HEADER; + case 4: + return IP_PACKET_DATA; + case 5: + return ARP; + case 6: + return SAP_6; + case 7: + return SAP_7; + case 8: + return SAP_8; + case 9: + return PROPRIETARY_DATA; + case 10: + return SHORT_DATA; + case 11: + return SAP_11; + case 12: + return SAP_12; + case 13: + return SAP_13; + case 14: + return SAP_14; + case 15: + return SAP_15; + default: + return UNKNOWN; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceFunction.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceFunction.java new file mode 100644 index 00000000..7f38e6da --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceFunction.java @@ -0,0 +1,60 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Tier III service functions + */ +public enum ServiceFunction +{ + ALL("ALL"), + REGISTRATION_AND_CHANNEL_GRANTS("REGISTRATION/CHANNEL GRANTS ONLY"), + REGISTRATION_NO_CHANNEL_GRANTS("REGISTRATION/NO CHANNEL GRANTS"), + REGISTRATION_ONLY("REGISTRATION ONLY"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + ServiceFunction(String label) + { + mLabel = label; + } + + /** + * Utility method to lookup the model type from the integer value + * @param value 0-3 + * @return entry or UNKNOWN + */ + public static ServiceFunction fromValue(int value) + { + if(0 <= value && value <= 3) + { + return ServiceFunction.values()[value]; + } + + return UNKNOWN; + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceKind.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceKind.java new file mode 100644 index 00000000..5a7b0dd9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceKind.java @@ -0,0 +1,62 @@ +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Service Kind enumeration for Tier III Ahoy Messages + * + * See: TS 102 361.4 paragraph 7.2.12 + */ +public enum ServiceKind +{ + INDIVIDUAL_VOICE_CALL_SERVICE(0, "INDIVIDUAL VOICE CALL SERVICE"), + TALKGROUP_VOICE_CALL_SERVICE(1, "TALKGROUP VOICE CALL SERVICE"), + INDIVIDUAL_PACKET_CALL_SERVICE(2, "INDIVIDUAL PACKET CALL SERVICE"), + TALKGROUP_PACKET_CALL_SERVICE(3, "TALKGROUP PACKET CALL SERVICE"), + INDIVIDUAL_UDT_SHORT_DATA_CALL_SERVICE(4, "INDIVIDUAL UDT SHORT DATA CALL SERVICE"), + TALKGROUP_UDT_SHORT_DATA_CALL_SERVICE(5, "TALKGROUP UDT SHORT DATA CALL SERVICE"), + UDT_SHORT_DATA_POLLING_SERVICE(6, "UDT SHORT DATA POLLING SERVICE"), + STATUS_TRANSPORT_SERVICE(7, "STATUS TRANSPORT SERVICE"), + CALL_DIVERSION_SERVICE(8, "CALL DIVERSION SERVICE"), + CALL_ANSWER_SERVICE(9, "CALL ANSWER SERVICE"), + FULL_DUPLEX_MS_TO_MS_VOICE_CALL_SERVICE(10, "FULL DUPLEX MS TO MS VOICE CALL SERVICE"), + FULL_DUPLEX_MS_TO_MS_PACKET_CALL_SERVICE(11, "FULL DUPLEX MS TO MS PACKET CALL SERVICE"), + RESERVED_12(12, "RESERVED 12"), + SUPPLEMENTARY_SERVICE(13, "SUPPLEMENTARY SERVICE"), + AUTHENTICATE_REGISTER_RADIO_CHECK_SERVICE(14, "REGISTRATION OR RADIO CHECK SERVICE"), + CANCEL_CALL_SERVICE(15, "CANCEL CALL SERVICE"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param value of the entry + * @param label for the entry + */ + ServiceKind(int value, String label) + { + mValue = value; + mLabel = label; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the type from a value. + * @param value of the service type + * @return type or UNKNOWN + */ + public static ServiceKind fromValue(int value) + { + if(0 <= value && value <= 15) + { + return ServiceKind.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java new file mode 100644 index 00000000..96e62a6d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java @@ -0,0 +1,136 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import com.google.common.base.Joiner; + +import java.util.ArrayList; +import java.util.List; + +/** + * DMR Service Options parsing class + */ +public class ServiceOptions +{ + private static final int EMERGENCY_MASK = 0x80; + private static final int ENCRYPTION_MASK = 0x40; + private static final int BROADCAST_SERVICE_MASK = 0x8; + private static final int OPEN_VOICE_CALL_MODE_MASK = 0x4; + private static final int PRIORITY_MASK = 0x3; + private int mValue; + + /** + * Constructs an instance + * @param value of the service options bitmap + */ + public ServiceOptions(int value) + { + mValue = value; + } + + /** + * Indicates if this is an emergency communication + */ + public boolean isEmergency() + { + return isSet(EMERGENCY_MASK); + } + + /** + * Indicates if the call is encrypted + */ + public boolean isEncrypted() + { + return isSet(ENCRYPTION_MASK); + } + + /** + * Indicates if this is broadcast service + */ + public boolean isBroadcastService() + { + return isSet(BROADCAST_SERVICE_MASK); + } + + /** + * Indicates if the call is using open voice call mode + */ + public boolean isOpenVoiceCallMode() + { + return isSet(OPEN_VOICE_CALL_MODE_MASK); + } + + /** + * Priority of the communication + * @return priority 0-3, 0 = lowest/routine, 3 = highest + */ + public int getPriority() + { + return (mValue & PRIORITY_MASK); + } + + /** + * Indicates if the bits corresponding to the mask value are set in the service options bitmap value. + */ + private boolean isSet(int mask) + { + return (mValue & mask) == mask; + } + + @Override + public String toString() + { + List flags = new ArrayList<>(); + + if(isEmergency()) + { + flags.add("EMERGENCY"); + } + + if(isEncrypted()) + { + flags.add("ENCRYPTED"); + } + + if(isBroadcastService()) + { + flags.add("BROADCAST"); + } + + if(isOpenVoiceCallMode()) + { + flags.add("OVCM"); + } + + if(getPriority() > 0) + { + flags.add("PRIORITY-" + getPriority()); + } + + StringBuilder sb = new StringBuilder(); + sb.append("SERVICE OPTIONS ["); + if(!flags.isEmpty()) + { + sb.append(Joiner.on(",").join(flags)); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceType.java new file mode 100644 index 00000000..d0c634d2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceType.java @@ -0,0 +1,63 @@ +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Service Type enumeration for Unified Single Block Data (USBD) messages + * + * See: TS 102 361-1 and TS 102 361.4 + */ +public enum ServiceType +{ + LIP_SHORT_LOCATION_REQUEST(0, "LIP SHORT LOCATION REQUEST"), + RESERVED_1(1, "RESERVED 1"), + RESERVED_2(2, "RESERVED 2"), + RESERVED_3(3, "RESERVED 3"), + RESERVED_4(4, "RESERVED 4"), + RESERVED_5(5, "RESERVED 5"), + RESERVED_6(6, "RESERVED 6"), + RESERVED_7(7, "RESERVED 7"), + VENDOR_SERVICE_1(8, "VENDOR SERVICE 1"), + VENDOR_SERVICE_2(9, "VENDOR SERVICE 2"), + VENDOR_SERVICE_3(10, "VENDOR SERVICE 3"), + VENDOR_SERVICE_4(11, "VENDOR SERVICE 4"), + VENDOR_SERVICE_5(12, "VENDOR SERVICE 5"), + VENDOR_SERVICE_6(13, "VENDOR SERVICE 6"), + VENDOR_SERVICE_7(14, "VENDOR SERVICE 7"), + VENDOR_SERVICE_8(15, "VENDOR SERVICE 8"), + + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + /** + * Constructs an instance + * @param value of the entry + * @param label for the entry + */ + ServiceType(int value, String label) + { + mValue = value; + mLabel = label; + } + + @Override + public String toString() + { + return mLabel; + } + + /** + * Utility method to lookup the type from a value. + * @param value of the service type + * @return type or UNKNOWN + */ + public static ServiceType fromValue(int value) + { + if(0 <= value && value <= 15) + { + return ServiceType.values()[value]; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/SystemIdentityCode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/SystemIdentityCode.java new file mode 100644 index 00000000..7b596979 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/SystemIdentityCode.java @@ -0,0 +1,153 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork; +import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; + +/** + * DMR Tier III System Identity Code structure parser + */ +public class SystemIdentityCode extends AbstractStructure +{ + private static final int[] MODEL = new int[]{0, 1}; + private static final int[] TINY_NET = new int[]{2, 3, 4, 5, 6, 7, 8, 9, 10}; + private static final int[] TINY_SITE = new int[]{11, 12, 13}; + private static final int[] SMALL_NET = new int[]{2, 3, 4, 5, 6, 7, 8}; + private static final int[] SMALL_SITE = new int[]{9, 10, 11, 12, 13}; + private static final int[] LARGE_NET = new int[]{2, 3, 4, 5}; + private static final int[] LARGE_SITE = new int[]{6, 7, 8, 9, 10, 11, 12, 13}; + private static final int[] HUGE_NET = new int[]{2, 3}; + private static final int[] HUGE_SITE = new int[]{4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + private static final int[] PAR_SUBFIELD = new int[]{14,15}; + + private DMRNetwork mNetwork; + private DMRSite mSite; + private boolean mHasPAR; + + /** + * Constructs an instance + * @param message containing the system identity code + * @param offset into the message to the start of the structure + * @param hasPAR indicates if the structure includes the two least significant bits for the PAR value. + */ + public SystemIdentityCode(CorrectedBinaryMessage message, int offset, boolean hasPAR) + { + super(message, offset); + mHasPAR = hasPAR; + } + + /** + * Indicates if this structure has the PAR field that reflects if the site has a single control channel or + * two control channels and the mobile subscribers are divided into categories A and/or B. + * @return + */ + public boolean hasPAR() + { + return mHasPAR; + } + + /** + * PAR reflects if the site has a single control channel or two control channels and the mobile subscribers are + * divided into categories A and/or B + */ + public PAR getPAR() + { + if(hasPAR()) + { + return PAR.fromValue(getMessage().getInt(PAR_SUBFIELD, getOffset())); + } + + return PAR.UNKNOWN; + } + + /** + * Network model structure + */ + public Model getModel() + { + return Model.fromValue(getMessage().getInt(MODEL, getOffset())); + } + + /** + * Network identifier + */ + public DMRNetwork getNetwork() + { + if(mNetwork == null) + { + switch(getModel()) + { + case TINY: + mNetwork = new DMRNetwork(getMessage().getInt(TINY_NET, getOffset())); + break; + case SMALL: + mNetwork = new DMRNetwork(getMessage().getInt(SMALL_NET, getOffset())); + break; + case LARGE: + mNetwork = new DMRNetwork(getMessage().getInt(LARGE_NET, getOffset())); + break; + case HUGE: + mNetwork = new DMRNetwork(getMessage().getInt(HUGE_NET, getOffset())); + break; + } + } + + return mNetwork; + } + + /** + * Site identifier + */ + public DMRSite getSite() + { + if(mSite == null) + { + switch(getModel()) + { + case TINY: + mSite = new DMRSite(getMessage().getInt(TINY_SITE, getOffset())); + break; + case SMALL: + mSite = new DMRSite(getMessage().getInt(SMALL_SITE, getOffset())); + break; + case LARGE: + mSite = new DMRSite(getMessage().getInt(LARGE_SITE, getOffset())); + break; + case HUGE: + mSite = new DMRSite(getMessage().getInt(HUGE_SITE, getOffset())); + break; + } + } + + return mSite; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(getModel()); + sb.append(" NETWORK:").append(getNetwork().getValue()); + sb.append(" SITE:").append(getSite().getValue()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/UnifiedDataTransportFormat.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/UnifiedDataTransportFormat.java new file mode 100644 index 00000000..336513d0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/UnifiedDataTransportFormat.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * Payload Formats for Unified Data Transport (UDT) packets + */ +public enum UnifiedDataTransportFormat +{ + BINARY("BINARY"), + MOBILE_SUBSCRIBER_OR_TALKGROUP_ADDRESS("MS OR TG ADDRESS"), + BCD_4_BITS("4-BIT BCD"), + ASCII_7("ISO 7-BIT"), + ASCII_8("ISO 8-BIT"), + NMEA_GPS_LOCATION_CODED("NMEA GPS CODED"), + IP_ADDRESS("IP ADDRESS"), + UNICODE_16("16-BIT UNICODE"), + VENDOR_PROPRIETARY_8("VENDOR SPECIFIC-8"), + VENDOR_PROPRIETARY_9("VENDOR SPECIFIC-9"), + MIXED_FORMAT("MIXED ADDRESS AND UNICODE"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + UnifiedDataTransportFormat(String label) + { + mLabel = label; + } + + public static UnifiedDataTransportFormat fromValue(int value) + { + switch(value) + { + case 0: + return BINARY; + case 1: + return MOBILE_SUBSCRIBER_OR_TALKGROUP_ADDRESS; + case 2: + return BCD_4_BITS; + case 3: + return ASCII_7; + case 4: + return ASCII_8; + case 5: + return NMEA_GPS_LOCATION_CODED; + case 6: + return IP_ADDRESS; + case 7: + return UNICODE_16; + case 8: + return VENDOR_PROPRIETARY_8; + case 9: + return VENDOR_PROPRIETARY_9; + case 10: + return MIXED_FORMAT; + default: + return UNKNOWN; + } + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Vendor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Vendor.java new file mode 100644 index 00000000..009285ba --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Vendor.java @@ -0,0 +1,99 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Enumeration of DMR vendors aka Feature IDs (FID) + */ +public enum Vendor +{ + STANDARD(0, "STANDARD"), + FYLDE_MICRO(4, "FYLDE MICRO"), + PROD_EL_SPA(5, "PROD-EL SPA"), + MOTOROLA_CONNECT_PLUS(6, "MOTOROLA CON+"), + RADIO_DATA_GMBH(7, "RADIO DATA GMBH"), + HYTERA_8(8, "HYTERA"), + MOTOROLA_CAPACITY_PLUS(16, "MOTOROLA CAP+"), + EMC_SPA_19(19, "EMC SPA"), + EMC_SPA_28(28, "EMC SPA"), + RADIO_ACTIVITY_SRL_51(51, "RADIO ACTIVITY SRL"), + RADIO_ACTIVITY_SRL_60(60, "RADIO ACTIVITY SRL"), + TAIT(88, "TAIT"), + HYTERA_68(104, "HYTERA"), + VERTEX_STANDARD(119, "VERTEX STANDARD"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + Vendor(int value, String label) + { + mValue = value; + mLabel = label; + } + + public int getValue() + { + return mValue; + } + + public String getLabel() + { + return mLabel; + } + + @Override + public String toString() + { + return getLabel(); + } + + /** + * Map of integer values to corresponding vendor entries + */ + private static final Map LOOKUP_MAP = new TreeMap<>(); + + /** + * Static loading of the lookup map + */ + static + { + for(Vendor vendor: Vendor.values()) + { + LOOKUP_MAP.put(vendor.getValue(), vendor); + } + } + + /** + * Lookup the vendor from an integer value + */ + public static Vendor fromValue(int value) + { + if(LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Version.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Version.java new file mode 100644 index 00000000..2f659223 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/Version.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.type; + +/** + * DMR Tier III ETSI 102 361-4 ICD Versions + */ +public enum Version +{ + VERSION_0("1.0.0-1.5.1"), + VERSION_1("1.6.1"), + VERSION_2("1.7.1-1.9.1"), + VERSION_3("RESERVED 3"), + VERSION_4("RESERVED 4"), + VERSION_5("RESERVED 5"), + VERSION_6("RESERVED 6"), + VERSION_7("RESERVED 7"), + UNKNOWN("UNKNOWN"); + + private String mLabel; + + Version(String label) + { + mLabel = label; + } + + /** + * Utility method to lookup the model type from the integer value + * @param value 0-7 + * @return entry or UNKNOWN + */ + public static Version fromValue(int value) + { + if(0 <= value && value <= 7) + { + return Version.values()[value]; + } + + return UNKNOWN; + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java new file mode 100644 index 00000000..dddebb0a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/DMRAudioModule.java @@ -0,0 +1,339 @@ +/* + * + * * ****************************************************************************** + * * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao + * * + * * This 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 + * * ***************************************************************************** + * + * + */ +package io.github.dsheirer.module.decode.dmr.message.voice; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; +import io.github.dsheirer.audio.squelch.SquelchState; +import io.github.dsheirer.audio.squelch.SquelchStateEvent; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierUpdateNotification; +import io.github.dsheirer.identifier.IdentifierUpdateProvider; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.tone.AmbeTone; +import io.github.dsheirer.identifier.tone.Tone; +import io.github.dsheirer.identifier.tone.ToneSequence; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.identifier.DMRToneIdentifier; +import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; +import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; +import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.sample.Listener; +import jmbe.iface.IAudioWithMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * DMR Audio Module for converting transmitted AMBE audio frames to 8 kHz PCM audio + */ +public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider +{ + private final static Logger mLog = LoggerFactory.getLogger(DMRAudioModule.class); + private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); + private ToneMetadataProcessor mToneMetadataProcessor = new ToneMetadataProcessor(); + private Listener mIdentifierUpdateNotificationListener; + private List mQueuedAmbeFrames = new ArrayList<>(); + private boolean mEncryptedCallStateEstablished = false; + private boolean mEncryptedCall = false; + + /** + * Constructs an instance + * @param userPreferences for JMBE library location + * @param aliasList for audio + * @param timeslot for this audio module + */ + public DMRAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot) + { + super(userPreferences, aliasList, timeslot); + } + + @Override + public Listener getSquelchStateListener() + { + return mSquelchStateListener; + } + + @Override + public void reset() + { + //Explicitly clear FROM identifiers to ensure previous call TONE identifiers are cleared. + mIdentifierCollection.remove(Role.FROM); + + mEncryptedCall = false; + mEncryptedCallStateEstablished = false; + mQueuedAmbeFrames.clear(); + } + + @Override + public void start() + { + } + + /** + * Processes DMR AMBE audio frame + */ + public void receive(IMessage message) + { + if(hasAudioCodec() && message.getTimeslot() == getTimeslot()) + { + //Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames + //in the EMB field. + if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage) + { + VoiceEMBMessage voiceMessage = (VoiceEMBMessage)message; + if(voiceMessage.getEMB().isValid()) + { + mEncryptedCallStateEstablished = true; + mEncryptedCall = voiceMessage.getEMB().isEncrypted(); + } + + List frames = voiceMessage.getAMBEFrames(); + for(byte[] frame: frames) + { + processAudio(frame); + } + } + else if(message instanceof VoiceMessage) + { + VoiceMessage voiceMessage = (VoiceMessage)message; + List frames = voiceMessage.getAMBEFrames(); + for(byte[] frame: frames) + { + processAudio(frame); + } + } + else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader) + { + LCMessage lc = ((VoiceHeader)message).getLCMessage(); + + if(lc instanceof FullLCMessage) //We know it is, but this null checks as well + { + FullLCMessage flc = (FullLCMessage)lc; + + if(flc.isValid()) + { + mEncryptedCallStateEstablished = true; + mEncryptedCall = flc.isEncrypted(); + } + } + } + //Note: the DMRMessageProcess extracts Full Link Control messages from Voice Frames B-C and sends them + // independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of + // an ongoing call and can be used to establish encryption state when the FLC is a voice channel user. + else if(!mEncryptedCallStateEstablished && message instanceof AbstractVoiceChannelUser) + { + ServiceOptions serviceOptions = ((AbstractVoiceChannelUser)message).getServiceOptions(); + mEncryptedCallStateEstablished = true; + mEncryptedCall = serviceOptions.isEncrypted(); + } + else if(message instanceof Terminator) + { + reset(); + } + } + } + + /** + * Processes the audio frame. Queues the frame until encryption state is determined. Once determined, the audio + * frames are dequeued and audio is generated. + */ + private void processAudio(byte[] frame) + { + if(mEncryptedCallStateEstablished) + { + //Process any ambe frames that were queued awaiting encryption state determination + if(!mQueuedAmbeFrames.isEmpty()) + { + if(!mEncryptedCall) + { + for(byte[] queuedFrame: mQueuedAmbeFrames) + { + produceAudio(queuedFrame); + } + + mQueuedAmbeFrames.clear(); + } + + mQueuedAmbeFrames.clear(); + } + + produceAudio(frame); + } + else + { + mQueuedAmbeFrames.add(frame); + } + } + + private void produceAudio(byte[] frame) + { + try + { + IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame); + addAudio(audioWithMetadata.getAudio()); + processMetadata(audioWithMetadata); + } + catch(Exception e) + { + mLog.error("Error synthesizing DMR AMBE audio - continuing [" + e.getMessage() + "]"); + } + } + + /** + * Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the + * tone metadata can be converted into a FROM identifier and included with any call segment. + */ + private void processMetadata(IAudioWithMetadata audioWithMetadata) + { + if(audioWithMetadata.hasMetadata()) + { + //JMBE only places 1 entry in the map, but for consistency we'll process the map entry set + for(Map.Entry entry: audioWithMetadata.getMetadata().entrySet()) + { + //Each metadata map entry contains a tone-type (key) and tone (value) + Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); + + if(metadataIdentifier != null) + { + broadcast(metadataIdentifier); + } + } + } + else + { + mToneMetadataProcessor.closeMetadata(); + } + } + + /** + * Broadcasts the identifier to a registered listener + */ + private void broadcast(Identifier identifier) + { + if(mIdentifierUpdateNotificationListener != null) + { + mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier, + IdentifierUpdateNotification.Operation.ADD, getTimeslot())); + } + } + + /** + * Registers the listener to receive identifier updates + */ + @Override + public void setIdentifierUpdateListener(Listener listener) + { + mIdentifierUpdateNotificationListener = listener; + } + + /** + * Unregisters a listener from receiving identifier updates + */ + @Override + public void removeIdentifierUpdateListener() + { + mIdentifierUpdateNotificationListener = null; + } + + /** + * Wrapper for squelch state to process end of call actions. At call end the encrypted call state established + * flag is reset so that the encrypted audio state for the next call can be properly detected and we send an + * END audio packet so that downstream processors like the audio recorder can properly close out a call sequence. + */ + public class SquelchStateListener implements Listener + { + @Override + public void receive(SquelchStateEvent event) + { + if(event.getTimeslot() == getTimeslot() && event.getSquelchState() == SquelchState.SQUELCH) + { + closeAudioSegment(); + } + } + } + + /** + * Process AMBE audio frame tone metadata. Tracks the count of sequential frames containing tone metadata to + * provide a list of each unique tone and a time duration (milliseconds) for the tone. Tones are concatenated into + * a comma separated list and included as call segment metadata. + */ + public class ToneMetadataProcessor + { + private List mTones = new ArrayList<>(); + private Tone mCurrentTone; + + /** + * Resets or clears any accumulated call tone sequences to prepare for the next call. + */ + public void reset() + { + mTones.clear(); + } + + /** + * Process the tone metadata + * @param type of tone + * @param value of tone + * @return an identifier with the accumulated tone metadata set + */ + public Identifier process(String type, String value) + { + if(type == null || value == null) + { + return null; + } + + AmbeTone tone = AmbeTone.fromValues(type, value); + + if(tone == AmbeTone.INVALID) + { + return null; + } + + if(mCurrentTone == null || mCurrentTone.getAmbeTone() != tone) + { + mCurrentTone = new Tone(tone); + mTones.add(mCurrentTone); + } + + mCurrentTone.incrementDuration(); + + return DMRToneIdentifier.create(new ToneSequence(new ArrayList<>(mTones))); + } + + /** + * Closes current tone metadata when there is no metadata for the current audio frame. + */ + public void closeMetadata() + { + mCurrentTone = null; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/EMB.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/EMB.java new file mode 100644 index 00000000..c721b668 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/EMB.java @@ -0,0 +1,183 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.dmr.message.voice; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.message.type.LCSS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; + +/** + * DMR Voice Frame B-F Embedded Signalling Chunk + */ +public class EMB +{ + private final static Logger mLog = LoggerFactory.getLogger(EMB.class); + public static final int[] CRC_CHECKSUMS = new int[]{0x02F, 0x11E, 0x1B7, 0x1E2, 0x1C9, 0x0E5, 0x073}; + private static final int[] VALID_WORDS = new int[]{ + 0x0000, 0x0273, 0x04E5, 0x0696, 0x09C9, 0x0BBA, 0x0D2C, 0x0F5F, 0x11E2, 0x1391, 0x1507, 0x1774, 0x182B, 0x1A58, + 0x1CCE, 0x1EBD, 0x21B7, 0x23C4, 0x2552, 0x2721, 0x287E, 0x2A0D, 0x2C9B, 0x2EE8, 0x3055, 0x3226, 0x34B0, 0x36C3, + 0x399C, 0x3BEF, 0x3D79, 0x3F0A, 0x411E, 0x436D, 0x45FB, 0x4788, 0x48D7, 0x4AA4, 0x4C32, 0x4E41, 0x50FC, 0x528F, + 0x5419, 0x566A, 0x5935, 0x5B46, 0x5DD0, 0x5FA3, 0x60A9, 0x62DA, 0x644C, 0x663F, 0x6960, 0x6B13, 0x6D85, 0x6FF6, + 0x714B, 0x7338, 0x75AE, 0x77DD, 0x7882, 0x7AF1, 0x7C67, 0x7E14, 0x802F, 0x825C, 0x84CA, 0x86B9, 0x89E6, 0x8B95, + 0x8D03, 0x8F70, 0x91CD, 0x93BE, 0x9528, 0x975B, 0x9804, 0x9A77, 0x9CE1, 0x9E92, 0xA198, 0xA3EB, 0xA57D, 0xA70E, + 0xA851, 0xAA22, 0xACB4, 0xAEC7, 0xB07A, 0xB209, 0xB49F, 0xB6EC, 0xB9B3, 0xBBC0, 0xBD56, 0xBF25, 0xC131, 0xC342, + 0xC5D4, 0xC7A7, 0xC8F8, 0xCA8B, 0xCC1D, 0xCE6E, 0xD0D3, 0xD2A0, 0xD436, 0xD645, 0xD91A, 0xDB69, 0xDDFF, 0xDF8C, + 0xE086, 0xE2F5, 0xE463, 0xE610, 0xE94F, 0xEB3C, 0xEDAA, 0xEFD9, 0xF164, 0xF317, 0xF581, 0xF7F2, 0xF8AD, 0xFADE, + 0xFC48, 0xFE3B}; + private static final HashSet WORD_SET = new HashSet<>(); + + static + { + for(int word: VALID_WORDS) + { + WORD_SET.add(word); + } + } + + private static final int[] WORD = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] COLOR_CODE = new int[]{0, 1, 2, 3}; + private static final int ENCRYPTION_PI = 4; + private static final int[] LINK_CONTROL_START_STOP = new int[]{5, 6}; + + private CorrectedBinaryMessage mMessage; + private boolean mValid = true; + + /** + * Constructs an instance + * + * @param message with transmitted bits + */ + public EMB(CorrectedBinaryMessage message) + { + mMessage = message; + checkCRC(); + } + + /** + * Checks the EMB word using the Quadratic Residue(16,7,6) code as detailed in TS 102 361-1 B.3.2. + * + * Implementation Notes: + * There are 7 information bits protected by 9 parity bits with a Hamming distance of 6 meaning that this code can + * correct up to 3 bit errors. The VALID_WORDS constant is a list of all possible (128) valid words precalculated + * with their corresponding QR checksum values. When performing a CRC check, if the complete 16-bit value matches + * any of these valid words, then the value has no errors. Otherwise, XOR the received value against all valid + * words and select any valid word that differs from the received word by no more than 3 bit position differences. + */ + private void checkCRC() + { + int word = getMessage().getInt(WORD); + + if(WORD_SET.contains(word)) + { + mValid = true; + return; + } + + int bitErrors = 0; + + for(int validWord: VALID_WORDS) + { + bitErrors = Integer.bitCount(word ^ validWord); + + if(bitErrors <= 3) + { + getMessage().setInt(validWord, WORD); + getMessage().setCorrectedBitCount(bitErrors); + mValid = bitErrors <= 1; //Only accept up to 1 bit error for validity + return; + } + } + + mValid = false; + } + + /** + * Indicates if this message is valid and has passed CRC check + */ + public boolean isValid() + { + return mValid; + } + + /** + * Message bits + */ + protected CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Color code + */ + public int getColorCode() + { + return getMessage().getInt(COLOR_CODE); + } + + /** + * Link Control Start-Stop + */ + public LCSS getLCSS() + { + return LCSS.fromValue(getMessage().getInt(LINK_CONTROL_START_STOP)); + } + + /** + * Indicates if this call is encrypted + */ + public boolean isEncrypted() + { + return getMessage().get(ENCRYPTION_PI); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(" EMB-"); + sb.append(isEncrypted() ? "EN" : "UN"); + sb.append(getLCSS()); + sb.append("CC:").append(getColorCode()); + return sb.toString(); + } + + public static void main(String[] args) + { + CorrectedBinaryMessage m = new CorrectedBinaryMessage(16); + + for(int x = 0; x < Math.pow(2, 16); x++) + { + m.setInt(x, WORD); + + EMB emb = new EMB(m); + + if(!emb.isValid()) + { + mLog.debug("Invalid: " + x); + } + } + + + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java new file mode 100644 index 00000000..45188ce6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java @@ -0,0 +1,41 @@ +package io.github.dsheirer.module.decode.dmr.message.voice; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; + +import java.util.Collections; +import java.util.List; + +/** + * DMR Voice Frame A with embedded sync pattern. + */ +public class VoiceAMessage extends VoiceMessage +{ + /** + * Constructs an instance. + * + * @param syncPattern for the Voice A frame + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + */ + public VoiceAMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, + int timeslot) + { + super(syncPattern, message, cach, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(" ").append(getSyncPattern()); + return sb.toString(); + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceEMBMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceEMBMessage.java new file mode 100644 index 00000000..b6e8387a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceEMBMessage.java @@ -0,0 +1,92 @@ +package io.github.dsheirer.module.decode.dmr.message.voice; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; + +/** + * DMR Voice Frames B - F + */ +public class VoiceEMBMessage extends VoiceMessage +{ + private static final int[] EMB = new int[]{132, 133, 134, 135, 136, 137, 138, 139, 172, 173, 174, + 175, 176, 177, 178, 179}; + private static final int PAYLOAD_START = 140; + private static final int PAYLOAD_END = 172; + + private EMB mEMB; + + /** + * DMR message frame. This message is comprised of a 24-bit prefix and a 264-bit message frame. Outbound base + * station frames transmit a Common Announcement Channel (CACH) in the 24-bit prefix, whereas Mobile inbound frames + * do not use the 24-bit prefix. + * + * @param syncPattern + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + */ + public VoiceEMBMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, int timeslot) + { + super(syncPattern, message, cach, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(getEMB().isValid()) + { + sb.append("CC:").append(getEMB().getColorCode()); + } + else + { + sb.append("CC:?"); + } + + sb.append(" ").append(getSyncPattern().toString()); + + if(getEMB().isValid()) + { + if(getEMB().isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + } + else + { + sb.append(" [EMB CRC-ERROR]"); + } + + return sb.toString(); + } + + /** + * EMB field + */ + public EMB getEMB() + { + if(mEMB == null) + { + //Transfer the embedded signalling bits into a new binary message + CorrectedBinaryMessage segment = new CorrectedBinaryMessage(16); + + for(int x = 0; x < EMB.length; x++) + { + segment.set(x, getMessage().get(EMB[x])); + } + + mEMB = new EMB(segment); + } + + return mEMB; + } + + /** + * Embedded signalling full link control fragment + */ + public BinaryMessage getFLCFragment() + { + return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceMessage.java new file mode 100644 index 00000000..1107ff73 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceMessage.java @@ -0,0 +1,79 @@ +package io.github.dsheirer.module.decode.dmr.message.voice; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.message.CACH; +import io.github.dsheirer.module.decode.dmr.message.DMRBurst; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base DMR Voice Message + */ +public abstract class VoiceMessage extends DMRBurst +{ + /** + * DMR message frame. This message is comprised of a 24-bit prefix and a 264-bit message frame. Outbound base + * station frames transmit a Common Announcement Channel (CACH) in the 24-bit prefix, whereas Mobile inbound frames + * do not use the 24-bit prefix. + * + * @param syncPattern + * @param message containing 288-bit DMR message with preliminary bit corrections indicated. + */ + public VoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, + int timeslot) + { + super(syncPattern, message, cach, timestamp, timeslot); + } + + /** + * AMBE audio CODEC encoded frames. + */ + public List getAMBEFrames() + { + List frames = new ArrayList<>(); + byte[] frame_1 = new byte[9]; + byte[] frame_2 = new byte[9]; + byte[] frame_3 = new byte[9]; + for(int i = 0; i < 9; i++) + { + frame_1[i] = getMessage().getByte(24 + i * 8); + } + // copy first 4 byte + for(int i = 0; i < 4; i++) + { + frame_2[i] = getMessage().getByte(96 + i * 8); + } + // 4 bits and 4 bits + frame_3[4] = (byte)((getMessage().getByte(128) & 0xF0) | (getMessage().getByte(180) >> 4)); + // copy last 4 byte + for(int i = 0; i < 4; i++) + { + frame_2[5 + i] = getMessage().getByte(184 + i * 8); + } + + for(int i = 0; i < 9; i++) + { + frame_3[i] = getMessage().getByte(216 + i * 8); + } + frames.add(frame_1); + frames.add(frame_2); + frames.add(frame_3); + return frames; + } + + @Override + public boolean isValid() + { + return true; + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistory.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistory.java new file mode 100644 index 00000000..d6c36379 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistory.java @@ -0,0 +1,74 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.event; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.module.HistoryModule; +import io.github.dsheirer.sample.Listener; + +/** + * Decode event history module. Maintains a history of decode events and constrains the total history size. + */ +public class DecodeEventHistory extends HistoryModule implements IDecodeEventListener +{ + /** + * Constructs an instance + */ + public DecodeEventHistory(int historySize) + { + super(historySize); + } + + /** + * Implements the IDecodeEventListener interface - delegates to receive(event) method. + */ + @Override + public Listener getDecodeEventListener() + { + return this; + } + + /** + * Process preload data + */ + @Subscribe + public void process(DecodeEventHistoryPreloadData preloadData) + { + for(IDecodeEvent decodeEvent: preloadData.getData()) + { + receive(decodeEvent); + } + } + + /** + * Processes a request for decode event history and posts the response back + * to the processing chain event bus so that any of the modules can receive that history. + * + * Note: this is principally used by the DMR decoder for Capacity+ REST channel rotation to transfer the decode + * event history from a traffic channel conversion to the new rest channel. + * + * @param request for decode event history + */ + @Subscribe + public void process(DecodeEventHistoryRequest request) + { + getInterModuleEventBus().post(new DecodeEventHistoryResponse(this)); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryPreloadData.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryPreloadData.java new file mode 100644 index 00000000..eaff6aa8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryPreloadData.java @@ -0,0 +1,40 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.event; + +import io.github.dsheirer.controller.channel.event.PreloadDataContent; + +import java.util.List; + +/** + * Decode event history for pre-loading into a processing chain. + */ +public class DecodeEventHistoryPreloadData extends PreloadDataContent> +{ + /** + * Constructs an instance + * + * @param data to preload + */ + public DecodeEventHistoryPreloadData(List data) + { + super(data); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryRequest.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryRequest.java new file mode 100644 index 00000000..76f2e946 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryRequest.java @@ -0,0 +1,35 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.event; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request for decode event history from a processing chain for the specified channel. + */ +public class DecodeEventHistoryRequest extends ModuleEventBusMessage +{ + /** + * Constructs an instance + */ + public DecodeEventHistoryRequest() + { + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryResponse.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryResponse.java new file mode 100644 index 00000000..6c947194 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventHistoryResponse.java @@ -0,0 +1,47 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.event; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Response to decode event history request from a processing chain for the specified channel. + */ +public class DecodeEventHistoryResponse extends ModuleEventBusMessage +{ + private DecodeEventHistory mDecodeEventHistory; + + /** + * Constructs an instance + * @param history that has a decode event history + */ + public DecodeEventHistoryResponse(DecodeEventHistory history) + { + mDecodeEventHistory = history; + } + + /** + * Decode event history module + */ + public DecodeEventHistory getDecodeEventHistory() + { + return mDecodeEventHistory; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java index 44904fd6..d2c360a9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java @@ -59,7 +59,7 @@ public class DecodeEventModel extends AbstractTableModel implements Listener getEvents() + { + return new ArrayList<>(mEvents); + } public void dispose() { - MyEventBus.getEventBus().unregister(this); + MyEventBus.getGlobalEventBus().unregister(this); Iterator it = mEvents.iterator(); while(it.hasNext()) @@ -98,6 +105,16 @@ public class DecodeEventModel extends AbstractTableModel implements Listener events) + { + mEvents.clear(); + mEvents.addAll(events); + fireTableDataChanged(); + } + public void reset() { dispose(); diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java index 9ccad4b8..0c41ed55 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java @@ -53,6 +53,7 @@ import java.awt.Component; import java.awt.EventQueue; import java.text.DecimalFormat; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -64,7 +65,8 @@ public class DecodeEventPanel extends JPanel implements Listener { - - //Dispose the current column width monitor and recreate after we swap out the table model - mTableColumnWidthMonitor.dispose(); - mTable.setModel(processingChain != null ? processingChain.getDecodeEventModel() : mEmptyDecodeEventModel); - if(processingChain != null) { - updateCellRenderers(); + mCurrentEventHistory = processingChain.getDecodeEventHistory(); + mEventModel.clearAndSet(mCurrentEventHistory.getItems()); + processingChain.getDecodeEventHistory().addListener(mEventModel); + } + else + { + mCurrentEventHistory = null; + mEventModel.clearAndSet(Collections.emptyList()); } - - mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY); }); } diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java index e2cfabc7..974f3178 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java @@ -22,6 +22,7 @@ package io.github.dsheirer.module.decode.event; public enum DecodeEventType { + AFFILIATE("Affiliate"), ANNOUNCEMENT("Announcement"), AUTOMATIC_REGISTRATION_SERVICE("Motorola ARS"), CALL("Call"), @@ -55,6 +56,7 @@ public enum DecodeEventType NOTIFICATION("Notification"), PAGE("Page"), QUERY("Query"), + RADIO_CHECK("Radio Check"), REGISTER("Register"), REGISTER_ESN("ESN"), REQUEST("Request"), diff --git a/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityModel.java b/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityModel.java index 71bc630a..b597baca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityModel.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityModel.java @@ -21,6 +21,7 @@ */ package io.github.dsheirer.module.decode.event; +import io.github.dsheirer.filter.AllPassFilter; import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.StuffBitsMessage; @@ -30,6 +31,7 @@ import javax.swing.table.AbstractTableModel; import java.awt.EventQueue; import java.text.SimpleDateFormat; import java.util.LinkedList; +import java.util.List; public class MessageActivityModel extends AbstractTableModel implements Listener { @@ -46,39 +48,49 @@ public class MessageActivityModel extends AbstractTableModel implements Listener private SimpleDateFormat mSDFTime = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); - private FilterSet mMessageFilter; - - public MessageActivityModel(FilterSet messageFilter) - { - mMessageFilter = messageFilter; - } + private FilterSet mMessageFilterSet = new FilterSet<>(new AllPassFilter<>()); public MessageActivityModel() { } + /** + * Applies the filter set + */ + public void setFilters(FilterSet filterSet) + { + mMessageFilterSet = filterSet; + } + /** * Clears all messages from history */ public void clear() { - EventQueue.invokeLater(new Runnable() - { - @Override - public void run() + EventQueue.invokeLater(() -> { + mMessageItems.clear(); + fireTableDataChanged(); + }); + } + + /** + * Clears the current messages and loads the messages argument + */ + public void clearAndSet(List messages) + { + EventQueue.invokeLater(() -> { + mMessageItems.clear(); + fireTableDataChanged(); + for(IMessage message: messages) { - int messageCount = mMessageItems.size(); - - mMessageItems.clear(); - - fireTableRowsDeleted(0, messageCount - 1); + receive(message); } }); } - public FilterSet getMessageFilter() + public FilterSet getMessageFilterSet() { - return mMessageFilter; + return mMessageFilterSet; } public void dispose() @@ -121,7 +133,7 @@ public class MessageActivityModel extends AbstractTableModel implements Listener return; } - if(mMessageFilter.passes(message)) + if(mMessageFilterSet.passes(message)) { final MessageItem messageItem = new MessageItem(message); diff --git a/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityPanel.java b/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityPanel.java index d25323e5..532f5706 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityPanel.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/MessageActivityPanel.java @@ -24,6 +24,7 @@ import com.jidesoft.swing.JideSplitButton; import io.github.dsheirer.filter.FilterEditorPanel; import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.MessageHistory; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor; @@ -53,12 +54,17 @@ public class MessageActivityPanel extends JPanel implements Listener filter = (FilterSet) ((MessageActivityModel) mTable.getModel()) - .getMessageFilter(); + .getMessageFilterSet(); FilterEditorPanel panel = new FilterEditorPanel<>(filter); diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java index 9e76369c..d1bb8b7d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java @@ -34,7 +34,6 @@ import io.github.dsheirer.module.decode.fleetsync2.identifier.FleetsyncIdentifie import io.github.dsheirer.module.decode.fleetsync2.message.Fleetsync2Message; import io.github.dsheirer.protocol.Protocol; -import java.util.Iterator; import java.util.Set; import java.util.TreeSet; @@ -144,7 +143,7 @@ public class Fleetsync2DecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: reset(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/Packet.java b/src/main/java/io/github/dsheirer/module/decode/ip/Packet.java index 8392fcb0..3cee1e7d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/Packet.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/Packet.java @@ -20,11 +20,11 @@ package io.github.dsheirer.module.decode.ip; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; public abstract class Packet implements IPacket { - private BinaryMessage mMessage; + private CorrectedBinaryMessage mMessage; private int mOffset; /** @@ -32,7 +32,7 @@ public abstract class Packet implements IPacket * @param message containing the packet * @param offset to the packet within the message */ - public Packet(BinaryMessage message, int offset) + public Packet(CorrectedBinaryMessage message, int offset) { mMessage = message; mOffset = offset; @@ -41,7 +41,7 @@ public abstract class Packet implements IPacket /** * Underlying message that contains this packet */ - public BinaryMessage getMessage() + public CorrectedBinaryMessage getMessage() { return mMessage; } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java index 3954b990..5d771779 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java @@ -1,7 +1,7 @@ /* * ****************************************************************************** * sdrtrunk - * Copyright (C) 2014-2019 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 @@ -20,13 +20,17 @@ package io.github.dsheirer.module.decode.ip; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.module.decode.ip.ars.ARSPacket; import io.github.dsheirer.module.decode.ip.cellocator.MCGPHeader; import io.github.dsheirer.module.decode.ip.cellocator.MCGPMessageFactory; +import io.github.dsheirer.module.decode.ip.icmp.ICMPPacket; import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header; import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet; +import io.github.dsheirer.module.decode.ip.lrrp.LRRPPacket; import io.github.dsheirer.module.decode.ip.udp.UDPPacket; +import io.github.dsheirer.module.decode.ip.udp.UDPPort; +import io.github.dsheirer.module.decode.ip.xcmp.XCMPPacket; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDCPPacketHeader; import io.github.dsheirer.module.decode.p25.reference.IPHeaderCompression; @@ -42,7 +46,7 @@ public class PacketMessageFactory * @param offset to the IP packet within the message * @return constructed packet message parser */ - public static IPacket create(SNDCPPacketHeader sndcpPacketHeader, BinaryMessage message, int offset) + public static IPacket create(SNDCPPacketHeader sndcpPacketHeader, CorrectedBinaryMessage message, int offset) { if(sndcpPacketHeader.getIPHeaderCompression() == IPHeaderCompression.NONE) { @@ -62,6 +66,26 @@ public class PacketMessageFactory } } + /** + * Creates an IP packet message parser + * + * @param message containing an IP packet + * @param offset to the IP packet within the message + * @return constructed packet message parser + */ + public static IPacket create(CorrectedBinaryMessage message, int offset) + { + int version = IPV4Header.getIPVersion(message, offset); + + switch(version) + { + case 4: + return new IPV4Packet(message, offset); + default: + return new UnknownPacket(message, offset); + } + } + /** * Creates an IP packet protocol payload message parser. * @@ -70,10 +94,12 @@ public class PacketMessageFactory * @param offset to the start of the IP packet payload * @return constructed packet message parser */ - public static IPacket create(IPProtocol protocol, BinaryMessage binaryMessage, int offset) + public static IPacket create(IPProtocol protocol, CorrectedBinaryMessage binaryMessage, int offset) { switch(protocol) { + case ICMP: + return new ICMPPacket(binaryMessage, offset); case UDP: return new UDPPacket(binaryMessage, offset); default: @@ -88,32 +114,37 @@ public class PacketMessageFactory * @param destinationPort for the packet (to identify the protocol/format) * @param binaryMessage containing the IP/UDP payload * @param offset in the message to the start of the payload - * @return constructed packet messge parser + * @return constructed packet message parser */ - public static IPacket createUDPPayload(int sourcePort, int destinationPort, BinaryMessage binaryMessage, int offset) + public static IPacket createUDPPayload(UDPPort sourcePort, UDPPort destinationPort, CorrectedBinaryMessage binaryMessage, int offset) { - switch(destinationPort) + switch(destinationPort.getValue()) { - case 231: - //Cellocator + case 231: //Cellocator if(MCGPHeader.isCellocatorMessage(binaryMessage, offset)) { return MCGPMessageFactory.create(binaryMessage, offset); } break; case 4001: //Location Service - break; + return new LRRPPacket(binaryMessage, offset); case 4004: //XCMP Service - break; + return new XCMPPacket(binaryMessage, offset); case 4005: //Automatic Registration Service return new ARSPacket(binaryMessage, offset); case 4007: //Text Message Service break; case 4008: //Telemetry Service break; + case 4009: //Over The Air Programming (OTAP) + break; + case 4012: //Battery Management + break; + case 4013: //Job Ticket Server + break; } - switch(sourcePort) + switch(sourcePort.getValue()) { case 231: //Cellocator diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/UnknownCompressedHeaderPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/UnknownCompressedHeaderPacket.java index b7c07b42..d633bca0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/UnknownCompressedHeaderPacket.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/UnknownCompressedHeaderPacket.java @@ -26,6 +26,9 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDC import java.util.Collections; import java.util.List; +import java.util.Collections; +import java.util.List; + public class UnknownCompressedHeaderPacket implements IPacket { private SNDCPPacketHeader mSNDCPPacketHeader; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/UnknownPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/UnknownPacket.java index dd257118..707a0a80 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/UnknownPacket.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/UnknownPacket.java @@ -22,6 +22,7 @@ package io.github.dsheirer.module.decode.ip; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.identifier.Identifier; + import java.util.Collections; import java.util.List; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeader.java index 6f767e9a..8aeda15c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeader.java @@ -21,9 +21,12 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.Header; -public class ARSHeader extends Header +import java.util.List; + +public abstract class ARSHeader extends Header { private static final int[] LENGTH = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; private static final int HEADER_EXTENSION_FLAG = 16; @@ -43,19 +46,6 @@ public class ARSHeader extends Header super(message, offset); } - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("ARS ").append(getPDUType()); - - if(getPDUType() == ARSPDUType.UNKNOWN) - { - sb.append(" PACKET:").append(getMessage().toHexString()); - } - - return sb.toString(); - } - /** * Checks that the underlying message is long enough to contain the PDU */ @@ -141,4 +131,9 @@ public class ARSHeader extends Header return 16 + 8; } } + + /** + * Identifiers for this packet + */ + public abstract List getIdentifiers(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeaderFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeaderFactory.java index 1bf7f985..35f043e1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeaderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSHeaderFactory.java @@ -44,7 +44,7 @@ public class ARSHeaderFactory return new RegistrationAcknowledgement(message, offset); case UNKNOWN: default: - return new ARSHeader(message, offset); + return new UnknownARSHeader(message, offset); } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSPacket.java index 00f945e6..c465b287 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSPacket.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/ARSPacket.java @@ -20,11 +20,11 @@ package io.github.dsheirer.module.decode.ip.ars; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.Packet; -import java.util.Collections; + import java.util.List; public class ARSPacket extends Packet @@ -37,7 +37,7 @@ public class ARSPacket extends Packet * @param message containing the packet * @param offset to the packet within the message */ - public ARSPacket(BinaryMessage message, int offset) + public ARSPacket(CorrectedBinaryMessage message, int offset) { super(message, offset); } @@ -71,6 +71,6 @@ public class ARSPacket extends Packet @Override public List getIdentifiers() { - return Collections.emptyList(); + return getHeader().getIdentifiers(); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceDeRegistration.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceDeRegistration.java index 361211b3..ce0be02b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceDeRegistration.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceDeRegistration.java @@ -21,6 +21,10 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; public class DeviceDeRegistration extends ARSHeader { @@ -41,4 +45,10 @@ public class DeviceDeRegistration extends ARSHeader sb.append("ARS DEVICE/USER DE-REGISTRATION"); return sb.toString(); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistration.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistration.java index 8d3271ce..e219f9c2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistration.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistration.java @@ -21,9 +21,23 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSDevice; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSPassword; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + +/** + * Automatic Registration Service - Device Registration + */ public class DeviceRegistration extends ARSHeader { + private final static Logger mLog = LoggerFactory.getLogger(DeviceRegistration.class); + private static final int SECOND_HEADER_EXTENSION_FLAG = 24; private static final int[] EVENT = {25, 26}; private static final int[] ENCODING = {27, 28, 29, 30, 31}; @@ -32,6 +46,11 @@ public class DeviceRegistration extends ARSHeader private static final int[] BYTE_VALUE = {0, 1, 2, 3, 4, 5, 6, 7}; + private ARSDevice mDevice; + private ARSUser mUser; + private ARSPassword mPassword; + private List mIdentifiers; + /** * Constructs a parser for a header contained within a binary message starting at the offset. * @@ -41,6 +60,7 @@ public class DeviceRegistration extends ARSHeader public DeviceRegistration(BinaryMessage message, int offset) { super(message, offset); + parsePayload(); } public String toString() @@ -52,10 +72,23 @@ public class DeviceRegistration extends ARSHeader { if(hasHeaderExtension()) { - sb.append(isInitialEvent() ? "-INITIAL " : "-REFRESH "); + sb.append(isInitialEvent() ? "-INITIAL" : "-REFRESH"); } - sb.append(" ").append(getPayload()); + if(hasDevice()) + { + sb.append(" DEVICE:").append(getDevice()); + } + + if(hasUser()) + { + sb.append(" USER:").append(getUser()); + } + + if(hasPassword()) + { + sb.append(" PW:").append(getPassword()); + } } else { @@ -72,14 +105,62 @@ public class DeviceRegistration extends ARSHeader return getMessage().getInt(EVENT, getOffset()) == 1; } + /** + * ARS User Id + * @return user or null + */ + public ARSUser getUser() + { + return mUser; + } + + /** + * Indicates if this packet has a user Id + */ + public boolean hasUser() + { + return mUser != null; + } + + /** + * ARS Password + * @return password or null + */ + public ARSPassword getPassword() + { + return mPassword; + } + + /** + * Indicates if this packet has a password + */ + public boolean hasPassword() + { + return mPassword != null; + } + + /** + * ARS Device + * @return device or null + */ + public ARSDevice getDevice() + { + return mDevice; + } + + /** + * Indicates if this packet has a device Id + */ + public boolean hasDevice() + { + return mDevice != null; + } + /** * Device, user and password values contained in the registration packet. */ - public String getPayload() + private void parsePayload() { - StringBuilder sb = new StringBuilder(); - sb.append("DEVICE:"); - int pointer = getOffset(); if(hasHeaderExtension()) @@ -91,53 +172,62 @@ public class DeviceRegistration extends ARSHeader pointer += DEVICE_IDENTIFIER_START; } - int identifierSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int deviceIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer); - if(identifierSize > 0) + pointer += 8; + + if(deviceIdentifierSize > 0) { - for(int x = 0; x < identifierSize; x++) + StringBuilder sb = new StringBuilder(); + + for(int x = 0; x < deviceIdentifierSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; + } + + try + { + int device = Integer.parseInt(sb.toString()); + mDevice = ARSDevice.createFrom(device); + } + catch(Exception e) + { + mLog.error("Error parsing ARS device identifier from value [" + sb.toString() + "]"); } } - else - { - sb.append("(none)"); - } - sb.append(" USER:"); - - int userIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int userIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer); + pointer += 8; if(userIdentifierSize > 0) { + StringBuilder sb = new StringBuilder(); + for(int x = 0; x < userIdentifierSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; } - } - else - { - sb.append("(none)"); + + mUser = ARSUser.createFrom(sb.toString()); } - sb.append(" PASSWORD:"); - - int passwordSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int passwordSize = getMessage().getInt(BYTE_VALUE, pointer); + pointer += 8; if(passwordSize > 0) { + StringBuilder sb = new StringBuilder(); + for(int x = 0; x < passwordSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; } - } - else - { - sb.append("(none)"); - } - return sb.toString(); + mPassword = ARSPassword.createFrom(sb.toString()); + } } /** @@ -150,4 +240,30 @@ public class DeviceRegistration extends ARSHeader { return (char)getMessage().getByte(BYTE_VALUE, offset); } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasUser()) + { + mIdentifiers.add(getUser()); + } + + if(hasDevice()) + { + mIdentifiers.add(getDevice()); + } + + if(hasPassword()) + { + mIdentifiers.add(getPassword()); + } + } + + return mIdentifiers; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/QueryMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/QueryMessage.java index e1c08371..501a86ab 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/QueryMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/QueryMessage.java @@ -21,6 +21,10 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; public class QueryMessage extends ARSHeader { @@ -41,4 +45,10 @@ public class QueryMessage extends ARSHeader sb.append("ARS QUERY MESSAGE"); return sb.toString(); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/RegistrationAcknowledgement.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/RegistrationAcknowledgement.java index 6259d20c..c18fe4e9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/RegistrationAcknowledgement.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/RegistrationAcknowledgement.java @@ -21,6 +21,10 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; public class RegistrationAcknowledgement extends ARSHeader { @@ -93,4 +97,10 @@ public class RegistrationAcknowledgement extends ARSHeader { return getMessage().getInt(TIMER_OR_REASON, getOffset()); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistrationAcknowledge.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UnknownARSHeader.java similarity index 70% rename from src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistrationAcknowledge.java rename to src/main/java/io/github/dsheirer/module/decode/ip/ars/UnknownARSHeader.java index 4f24320a..575956ea 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/DeviceRegistrationAcknowledge.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UnknownARSHeader.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 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 @@ -15,14 +14,21 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; -public class DeviceRegistrationAcknowledge extends ARSHeader +import java.util.Collections; +import java.util.List; + +/** + * Unknown ARS Header + */ +public class UnknownARSHeader extends ARSHeader { /** * Constructs a parser for a header contained within a binary message starting at the offset. @@ -30,7 +36,7 @@ public class DeviceRegistrationAcknowledge extends ARSHeader * @param message containing the header * @param offset to the header within the message */ - public DeviceRegistrationAcknowledge(BinaryMessage message, int offset) + public UnknownARSHeader(BinaryMessage message, int offset) { super(message, offset); } @@ -38,7 +44,15 @@ public class DeviceRegistrationAcknowledge extends ARSHeader public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("DEVICE REGISTRATION ACKNOWLEDGED"); + sb.append("*UNKNOWN* ").append(getPDUType()); + sb.append(" PACKET:").append(getMessage().toHexString()); + return sb.toString(); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistration.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistration.java index 880994fe..97d478fe 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistration.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistration.java @@ -21,7 +21,14 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import java.util.Collections; +import java.util.List; + +/** + * Automatic Registration Service - User De-Registration + */ public class UserDeRegistration extends ARSHeader { /** @@ -38,7 +45,13 @@ public class UserDeRegistration extends ARSHeader public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("ARS USER DE-REGISTRATION"); + sb.append("USER DE-REGISTRATION"); return sb.toString(); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistrationAcknowledge.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistrationAcknowledge.java index 0cb2e478..b5914760 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistrationAcknowledge.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserDeRegistrationAcknowledge.java @@ -21,6 +21,10 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; public class UserDeRegistrationAcknowledge extends ARSHeader { @@ -38,7 +42,13 @@ public class UserDeRegistrationAcknowledge extends ARSHeader public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("ARS USER DE-REGISTRATION ACKNOWLEDGED"); + sb.append("USER DE-REGISTRATION ACKNOWLEDGED"); return sb.toString(); } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistration.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistration.java index 8208d8da..b42a86b8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistration.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistration.java @@ -21,9 +21,19 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSDevice; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSPassword; +import io.github.dsheirer.module.decode.ip.ars.identifier.ARSUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; public class UserRegistration extends ARSHeader { + private final static Logger mLog = LoggerFactory.getLogger(UserRegistration.class); private static final int HEADER_EXTENSION_FLAG = 24; private static final int[] EVENT = {25, 26}; private static final int[] ENCODING = {27, 28, 29, 30, 31}; @@ -32,6 +42,11 @@ public class UserRegistration extends ARSHeader private static final int[] BYTE_VALUE = {0, 1, 2, 3, 4, 5, 6, 7}; + private ARSDevice mDevice; + private ARSUser mUser; + private ARSPassword mPassword; + private List mIdentifiers; + /** * Constructs a parser for a header contained within a binary message starting at the offset. * @@ -41,6 +56,7 @@ public class UserRegistration extends ARSHeader public UserRegistration(BinaryMessage message, int offset) { super(message, offset); + parsePayload(); } public String toString() @@ -50,7 +66,20 @@ public class UserRegistration extends ARSHeader if(isValid()) { - sb.append(" ").append(getPayload()); + if(hasDevice()) + { + sb.append(" DEVICE:").append(getDevice()); + } + + if(hasUser()) + { + sb.append(" USER:").append(getUser()); + } + + if(hasPassword()) + { + sb.append(" PW:").append(getPassword()); + } } else { @@ -62,11 +91,8 @@ public class UserRegistration extends ARSHeader /** * Device, user and password values contained in the registration packet. */ - public String getPayload() + private void parsePayload() { - StringBuilder sb = new StringBuilder(); - sb.append("DEVICE:"); - int pointer = getOffset(); if(hasHeaderExtension()) @@ -78,53 +104,114 @@ public class UserRegistration extends ARSHeader pointer += DEVICE_IDENTIFIER_START; } - int identifierSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int deviceIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer); - if(identifierSize > 0) + pointer += 8; + + if(deviceIdentifierSize > 0) { - for(int x = 0; x < identifierSize; x++) + StringBuilder sb = new StringBuilder(); + + for(int x = 0; x < deviceIdentifierSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; + } + + try + { + int device = Integer.parseInt(sb.toString()); + mDevice = ARSDevice.createFrom(device); + } + catch(Exception e) + { + //Abort processing + return; } } - else - { - sb.append("(none)"); - } - sb.append(" USER:"); - - int userIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int userIdentifierSize = getMessage().getInt(BYTE_VALUE, pointer); + pointer += 8; if(userIdentifierSize > 0) { + StringBuilder sb = new StringBuilder(); + for(int x = 0; x < userIdentifierSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; } - } - else - { - sb.append("(none)"); + + mUser = ARSUser.createFrom(sb.toString()); } - sb.append(" PASSWORD:"); - - int passwordSize = getMessage().getInt(BYTE_VALUE, pointer += 8); + int passwordSize = getMessage().getInt(BYTE_VALUE, pointer); + pointer += 8; if(passwordSize > 0) { + StringBuilder sb = new StringBuilder(); + for(int x = 0; x < passwordSize; x++) { - sb.append(getCharacter(pointer += 8)); + sb.append(getCharacter(pointer)); + pointer += 8; } - } - else - { - sb.append("(none)"); - } - return sb.toString(); + mPassword = ARSPassword.createFrom(sb.toString()); + } + } + + /** + * ARS User Id + * @return user or null + */ + public ARSUser getUser() + { + return mUser; + } + + /** + * Indicates if this packet has a user Id + */ + public boolean hasUser() + { + return mUser != null; + } + + /** + * ARS Password + * @return password or null + */ + public ARSPassword getPassword() + { + return mPassword; + } + + /** + * Indicates if this packet has a password + */ + public boolean hasPassword() + { + return mPassword != null; + } + + /** + * ARS Device + * @return device or null + */ + public ARSDevice getDevice() + { + return mDevice; + } + + /** + * Indicates if this packet has a device Id + */ + public boolean hasDevice() + { + return mDevice != null; } /** @@ -137,4 +224,30 @@ public class UserRegistration extends ARSHeader { return (char)getMessage().getByte(BYTE_VALUE, offset); } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasUser()) + { + mIdentifiers.add(getUser()); + } + + if(hasDevice()) + { + mIdentifiers.add(getDevice()); + } + + if(hasPassword()) + { + mIdentifiers.add(getPassword()); + } + } + + return mIdentifiers; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistrationAcknowledge.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistrationAcknowledge.java index a3178957..5545f9cb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistrationAcknowledge.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/UserRegistrationAcknowledge.java @@ -21,6 +21,10 @@ package io.github.dsheirer.module.decode.ip.ars; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; + +import java.util.Collections; +import java.util.List; public class UserRegistrationAcknowledge extends ARSHeader { @@ -85,4 +89,10 @@ public class UserRegistrationAcknowledge extends ARSHeader return "NONE"; } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSDevice.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSDevice.java new file mode 100644 index 00000000..7909c7cc --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSDevice.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.ars.identifier; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * Automatic Registration Service - Device Identifier + */ +public class ARSDevice extends IntegerIdentifier +{ + /** + * Constructs an instance + * @param value of the device identifier + * @param role of the device (TO/FROM) + */ + public ARSDevice(int value, Role role) + { + super(value, IdentifierClass.USER, Form.ARS_DEVICE, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.ARS; + } + + /** + * Creates an ARS device with a FROM role + */ + public static ARSDevice createFrom(int value) + { + return new ARSDevice(value, Role.FROM); + } + + /** + * Creates an ARS device with a TO role + */ + public static ARSDevice createTo(int value) + { + return new ARSDevice(value, Role.TO); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSPassword.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSPassword.java new file mode 100644 index 00000000..c0babe7b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSPassword.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.ars.identifier; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.string.StringIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * Automatic Registration Service - User Password + */ +public class ARSPassword extends StringIdentifier +{ + /** + * Constructs an instance + * @param value of the password + * @param role of the password (TO/FROM) + */ + public ARSPassword(String value, Role role) + { + super(value, IdentifierClass.USER, Form.ARS_USER, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.ARS; + } + + /** + * Creates an ARS password with a FROM role + */ + public static ARSPassword createFrom(String value) + { + return new ARSPassword(value, Role.FROM); + } + + /** + * Creates an ARS password with a TO role + */ + public static ARSPassword createTo(String value) + { + return new ARSPassword(value, Role.TO); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSUser.java b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSUser.java new file mode 100644 index 00000000..a14533f3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ars/identifier/ARSUser.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.ars.identifier; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.string.StringIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * Automatic Registration Service - User Identifier + */ +public class ARSUser extends StringIdentifier +{ + /** + * Constructs an instance + * @param value of the user + * @param role of the user (TO/FROM) + */ + public ARSUser(String value, Role role) + { + super(value, IdentifierClass.USER, Form.ARS_USER, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.ARS; + } + + /** + * Creates an ARS user with a FROM role + */ + public static ARSUser createFrom(String value) + { + return new ARSUser(value, Role.FROM); + } + + /** + * Creates an ARS user with a TO role + */ + public static ARSUser createTo(String value) + { + return new ARSUser(value, Role.TO); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/AcknowledgeMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/AcknowledgeMessage.java index ee48d73b..f730c13e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/AcknowledgeMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/AcknowledgeMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import java.util.Calendar; import java.util.Date; @@ -62,7 +62,7 @@ public class AcknowledgeMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public AcknowledgeMessage(MCGPHeader header, BinaryMessage message, int offset) + public AcknowledgeMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ForwardedDataMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ForwardedDataMessage.java index 3e332eaf..642d6016 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ForwardedDataMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ForwardedDataMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Unit (Outbound) data being forwarded over the wireless interface to a central application @@ -46,7 +46,7 @@ public class ForwardedDataMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public ForwardedDataMessage(MCGPHeader header, BinaryMessage message, int offset) + public ForwardedDataMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/GenericCommandMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/GenericCommandMessage.java index e5b8c8bd..64ef9b72 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/GenericCommandMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/GenericCommandMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Controller (Inbound) Generic Command @@ -44,7 +44,7 @@ public class GenericCommandMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public GenericCommandMessage(MCGPHeader header, BinaryMessage message, int offset) + public GenericCommandMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/LocationStatusMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/LocationStatusMessage.java index 446c5407..b3bc3f3c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/LocationStatusMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/LocationStatusMessage.java @@ -19,11 +19,12 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import org.apache.commons.math3.util.FastMath; + import java.util.Calendar; import java.util.Date; import java.util.TimeZone; -import org.apache.commons.math3.util.FastMath; /** * Unit (Outbound) Status and Location @@ -101,7 +102,7 @@ public class LocationStatusMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public LocationStatusMessage(MCGPHeader header, BinaryMessage message, int offset) + public LocationStatusMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPMessageFactory.java index 3176f193..75f71455 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPMessageFactory.java @@ -20,7 +20,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Creates an MCGP packet and header for parsing the contents of the packet. @@ -33,7 +33,7 @@ public class MCGPMessageFactory * @param offset into the message for the start of the MCGP payload * @return MCGP packet message parser */ - public static MCGPPacket create(BinaryMessage message, int offset) + public static MCGPPacket create(CorrectedBinaryMessage message, int offset) { MCGPHeader header = new MCGPHeader(message, offset); int payloadOffset = offset + MCGPHeader.HEADER_LENGTH; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPPacket.java index 44b92a1a..2ad8d090 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPPacket.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/MCGPPacket.java @@ -20,10 +20,11 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.Packet; + import java.util.Collections; import java.util.List; @@ -41,7 +42,7 @@ public abstract class MCGPPacket extends Packet * @param message containing the packet * @param offset to the packet within the message */ - public MCGPPacket(MCGPHeader header, BinaryMessage message, int offset) + public MCGPPacket(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(message, offset); mHeader = header; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularRequestMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularRequestMessage.java index c905d9b8..0abb11f9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularRequestMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularRequestMessage.java @@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Inbound (from Controller) Modular Request Message @@ -44,7 +45,7 @@ public class ModularRequestMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public ModularRequestMessage(MCGPHeader header, BinaryMessage message, int offset) + public ModularRequestMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularResponseMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularResponseMessage.java index d8491512..4a0cd6be 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularResponseMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ModularResponseMessage.java @@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Modular Message @@ -47,7 +48,7 @@ public class ModularResponseMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public ModularResponseMessage(MCGPHeader header, BinaryMessage message, int offset) + public ModularResponseMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingCommandMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingCommandMessage.java index 5a34284d..4effb840 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingCommandMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingCommandMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import org.apache.commons.lang3.StringUtils; /** @@ -48,7 +48,7 @@ public class ProgrammingCommandMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public ProgrammingCommandMessage(MCGPHeader header, BinaryMessage message, int offset) + public ProgrammingCommandMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingStatusMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingStatusMessage.java index 0b0a2162..28ca6892 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingStatusMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/ProgrammingStatusMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; /** * Unit (Outbound) Programming Status/Response @@ -47,7 +47,7 @@ public class ProgrammingStatusMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public ProgrammingStatusMessage(MCGPHeader header, BinaryMessage message, int offset) + public ProgrammingStatusMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/UnknownMCGPMessage.java b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/UnknownMCGPMessage.java index 5609387e..32af8b13 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/UnknownMCGPMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/cellocator/UnknownMCGPMessage.java @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.ip.cellocator; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.UnknownPacket; @@ -37,7 +37,7 @@ public class UnknownMCGPMessage extends MCGPPacket * @param message containing the packet * @param offset to the packet within the message */ - public UnknownMCGPMessage(MCGPHeader header, BinaryMessage message, int offset) + public UnknownMCGPMessage(MCGPHeader header, CorrectedBinaryMessage message, int offset) { super(header, message, offset); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPHeader.java new file mode 100644 index 00000000..f55d3d17 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPHeader.java @@ -0,0 +1,94 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.icmp; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.module.decode.ip.Header; + +/** + * Internet Control Message Protocol (ICMP) Header + */ +public class ICMPHeader extends Header +{ + private static final int[] TYPE = {0, 1, 2, 3, 4, 5, 6, 7}; + private static final int[] CODE = {8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] TYPE_CODE = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the header + * @param offset to the header within the message + */ + public ICMPHeader(BinaryMessage message, int offset) + { + super(message, offset); + checkValid(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + ICMPTypeCode typeCode = getTypeCode(); + + if(typeCode != ICMPTypeCode.UNKNOWN) + { + sb.append("ICMP ").append(typeCode); + } + else + { + sb.append("ICMP UNKNOWN TYPE CODE:").append(getTypeCodeValue()); + } + return sb.toString(); + } + + public ICMPTypeCode getTypeCode() + { + return ICMPTypeCode.fromValue(getTypeCodeValue()); + } + + public int getTypeCodeValue() + { + return getMessage().getInt(TYPE_CODE, getOffset()); + } + + public int getType() + { + return getMessage().getInt(TYPE, getOffset()); + } + + public int getCode() + { + return getMessage().getInt(CODE, getOffset()); + } + + private void checkValid() + { + setValid(getMessage().size() >= getOffset() + getLength()); + } + + @Override + public int getLength() + { + return 64; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPPacket.java new file mode 100644 index 00000000..8a8b8f8f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPPacket.java @@ -0,0 +1,119 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.icmp; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.Packet; +import io.github.dsheirer.module.decode.ip.PacketMessageFactory; +import io.github.dsheirer.module.decode.ip.UnknownPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Internet Control Message Protocol (ICMP) Packet + */ +public class ICMPPacket extends Packet +{ + private ICMPHeader mHeader; + private IPacket mPayload; + private List mIdentifiers; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the packet + * @param offset to the packet within the message + */ + public ICMPPacket(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(getHeader().isValid()) + { + sb.append(getHeader()); + } + else + { + sb.append("INVALID HEADER"); + } + + sb.append(" PACKET:").append(getPayload()); + + return sb.toString(); + } + + @Override + public ICMPHeader getHeader() + { + if(mHeader == null) + { + mHeader = new ICMPHeader(getMessage(), getOffset()); + } + + return mHeader; + } + + @Override + public IPacket getPayload() + { + if(mPayload == null && getHeader().isValid()) + { + if(getHeader().isValid()) + { + int offset = getOffset(); + int headerLength = getHeader().getLength(); + int totalOffset = offset + headerLength; + mPayload = PacketMessageFactory.create(getMessage(), getOffset() + getHeader().getLength()); + } + else + { + //Set the offset to the message offset plus 64 bits for UDP header + mPayload = new UnknownPacket(getMessage(), getOffset() + getHeader().getLength()); + } + } + + return mPayload; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + //Roll up the identifiers from the payload packet + if(hasPayload()) + { + mIdentifiers.addAll(getPayload().getIdentifiers()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPTypeCode.java b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPTypeCode.java new file mode 100644 index 00000000..c759a90a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/icmp/ICMPTypeCode.java @@ -0,0 +1,139 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.icmp; + +import java.util.Map; +import java.util.TreeMap; + +/** + * ICMP Type:Code enumeration + */ +public enum ICMPTypeCode +{ + ECHO_REPLY(0,0,"ECHO REPLY"), + + //Type: 3 Destination Unreachable + DESTINATION_NETWORK_UNREACHABLE(3,0, "DESTINATION NETWORK UNREACHABLE"), + DESTINATION_HOST_UNREACHABLE(3,1, "DESTINATION HOST UNREACHABLE"), + DESTINATION_PROTOCOL_UNREACHABLE(3,2, "DESTINATION PROTOCOL UNREACHABLE"), + DESTINATION_PORT_UNREACHABLE(3,3, "DESTINATION PORT UNREACHABLE"), + FRAGMENTATION_REQUIRED(3,4, "FRAGMENTATION REQUIRED"), + SOURCE_ROUTE_FAILED(3,5, "SOURCE ROUTE FAILED"), + DESTINATION_NETWORK_UNKNOWN(3,6, "DESTINATION NETWORK UNKNOWN"), + DESTINATION_HOST_UNKNOWN(3,7, "DESTINATION HOST UNKNOWN"), + SOURCE_HOST_ISOLATED(3,8, "SOURCE HOST ISOLATED"), + NETWORK_ADMINISTRATIVELY_PROHIBITED(3,9, "NETWORK ADMINISTRATIVELY PROHIBITED"), + HOST_ADMINISTRATIVELY_PROHIBITED(3,10, "HOST ADMINISTRATIVELY PROHIBITED"), + NETWORK_UNREACHABLE_FOR_TOS(3,11, "NETWORK UNREACHABLE FOR TOS"), + HOST_UNREACHABLE_FOR_TOS(3,12, "HOST UNREACHABLE FOR TOS"), + COMMUNICATION_ADMINISTRATIVELY_PROHIBITED(3,13, "COMMUNICATION ADMINISTRATIVELY PROHIBITED"), + HOST_PRECEDENCE_VIOLATION(3,14, "HOST PRECEDENCE VIOLATION"), + PRECEDENCE_CUTOFF_IN_EFFECT(3,15, "PRECEDENCE CUTOFF IN EFFECT"), + + //Type: 5 Redirect Message + REDIRECT_DATAGRAM_FOR_THE_NETWORK(5,0, "REDIRECT DATAGRAM FOR THE NETWORK"), + REDIRECT_DATAGRAM_FOR_THE_HOST(5,1, "REDIRECT DATAGRAM FOR THE HOST"), + REDIRECT_DATAGRAM_FOR_THE_TOS_AND_NETWORK(5,2, "REDIRECT DATAGRAM FOR THE TOS & NETWORK"), + REDIRECT_DATAGRAM_FOR_THE_TOS_AND_HOST(5,3, "REDIRECT DATAGRAM FOR THE TOS & HOST"), + + //Type: 8 Echo Request + ECHO_REQUEST(8,0, "ECHO REQUEST"), + + //Type: 9 Router Advertisement + ROUTER_ADVERTISEMENT(9, 0, "ROUTER ADVERTISEMENT"), + + //Type: 10 Router Solicitation + ROUTER_SOLICITATION(10,0,"ROUTER SOLICITATION"), + + //Type: 11 Time Exceeded + TTL_EXPIRED_IN_TRANSIT(11, 0, "TTL EXPIRED IN TRANSIT"), + FRAGMENT_REASSEMBLY_TIME_EXCEEDED(11, 1, "FRAGMENT REASSEMBLY TIME EXCEEDED"), + + //Type: 12 Bad IP Header + IP_HEADER_ERROR_POINTER(12, 0, "IP HEADER ERROR - POINTER INDICATES ERROR"), + IP_HEADER_ERROR_MISSING_OPTION(12,1,"IP HEADER ERROR - MISSING REQUIRED OPTION"), + IP_HEADER_ERROR_BAD_LENGTH(12,2,"IP HEADER ERROR - BAD LENGTH"), + + //Type: 13 Timestamp + TIMESTAMP(13, 0, "TIMESTAMP"), + + //Type: 14 Timestamp Reply + TIMESTAMP_REPLY(14, 0, "TIMESTAMP REPLY"), + + //Type: 42 Extended Echo Request + EXTENDED_ECHO_REQUEST(42, 0, "EXTENDED ECHO REQUEST"), + + //Type: 43 Extended Echo Reply + EXTENDED_ECHO_REPLY_NO_ERROR(43, 0, "EXTENDED ECHO - NO ERROR"), + EXTENDED_ECHO_REPLY_MALFORMED_QUERY(43, 1, "EXTENDED ECHO - MALFORMED QUERY"), + EXTENDED_ECHO_REPLY_NO_INTERFACE(43, 2, "EXTENDED ECHO - NO SUCH INTERFACE"), + EXTENDED_ECHO_REPLY_NO_TABLE(43, 3, "EXTENDED ECHO - NO SUCH TABLE ENTRY"), + EXTENDED_ECHO_REPLY_MULTIPLE_INTERFACES(43, 4, "EXTENDED ECHO - MULTIPLE INTERFACES"), + + UNKNOWN(-1, -1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + ICMPTypeCode(int type, int code, String label) + { + mValue = (type << 8) + code; + mLabel = label; + } + + private static final Map LOOKUP_MAP = new TreeMap<>(); + + static + { + for(ICMPTypeCode type: ICMPTypeCode.values()) + { + LOOKUP_MAP.put(type.getValue(), type); + } + } + + /** + * Utility method to lookup the ICMP type:code entry from the combined 16-bit value form the ICMP header + * @param value to lookup + * @return entry or UNKNOWN + */ + public static ICMPTypeCode fromValue(int value) + { + if(LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } + + /** + * 16-bit value that is the type and code bytes combined. + */ + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return mLabel; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Header.java b/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Header.java index d05c8c11..5f9d3b2e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Header.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Header.java @@ -73,11 +73,11 @@ public class IPV4Header extends Header return; } - if(getMessage().size() < totalLength + getOffset()) - { - setValid(false); - return; - } +// if(getMessage().size() < totalLength + getOffset()) +// { +// setValid(false); +// return; +// } } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Packet.java b/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Packet.java index c404d3c7..f768127c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Packet.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/ipv4/IPV4Packet.java @@ -20,12 +20,13 @@ package io.github.dsheirer.module.decode.ip.ipv4; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.Packet; import io.github.dsheirer.module.decode.ip.PacketMessageFactory; import io.github.dsheirer.module.decode.ip.UnknownPacket; + import java.util.ArrayList; import java.util.List; @@ -44,7 +45,7 @@ public class IPV4Packet extends Packet * @param message containing an IPV4 packet * @param offset to the start of the packet within the binary message */ - public IPV4Packet(BinaryMessage message, int offset) + public IPV4Packet(CorrectedBinaryMessage message, int offset) { super(message, offset); } @@ -120,14 +121,14 @@ public class IPV4Packet extends Packet if(mIdentifiers == null) { mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getHeader().getFromAddress()); + mIdentifiers.add(getHeader().getToAddress()); + //Roll up the identifiers from the payload packet if(hasPayload()) { mIdentifiers.addAll(getPayload().getIdentifiers()); } - - mIdentifiers.add(getHeader().getToAddress()); - mIdentifiers.add(getHeader().getFromAddress()); } return mIdentifiers; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPHeader.java new file mode 100644 index 00000000..0b5b8c29 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPHeader.java @@ -0,0 +1,87 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.Header; + +import java.util.Collections; +import java.util.List; + +/** + * Location Request/Response Protocol (LRRP) Header + */ +public class LRRPHeader extends Header +{ + private static final int[] TYPE = {0, 1, 2, 3, 4, 5, 6, 7}; + private static final int[] LENGTH = {8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the header + * @param offset to the header within the message + */ + public LRRPHeader(BinaryMessage message, int offset) + { + super(message, offset); + } + + /** + * Checks that the underlying message is long enough to contain the PDU + */ + private void checkValid() + { + setValid(getMessage().size() >= (getOffset() + getLength() + getPayloadLength())); + } + + /** + * Length of the payload in 8-bit characters + * + * @return + */ + public int getPayloadLength() + { + return getMessage().getInt(LENGTH, getOffset()); + } + + /** + * Length of this header in bits + * + * Note this length value includes the first 2 bytes of the message that indicates the overall + * message length and the length of the PDU header byte(s). + * + * This method may need to be overridden if there is more than 1 extension header in a PDU. + */ + @Override + public int getLength() + { + return 16; + } + + /** + * Identifiers for this packet + */ + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPPacket.java new file mode 100644 index 00000000..8cdd3686 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/LRRPPacket.java @@ -0,0 +1,133 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.Packet; +import io.github.dsheirer.module.decode.ip.lrrp.token.Token; +import io.github.dsheirer.module.decode.ip.lrrp.token.TokenFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Location Request/Response Protocol (LRRP) Packet + */ +public class LRRPPacket extends Packet +{ + //Constant for parsing a hexadecimal byte token identifier + private static final int[] BYTE_VALUE = {0, 1, 2, 3, 4, 5, 6, 7}; + public static final int TOKEN_START = 16; + + private LRRPHeader mHeader; + private List mTokens; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the packet + * @param offset to the packet within the message + */ + public LRRPPacket(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("LRRP"); + + for(Token token: getTokens()) + { + sb.append(" ").append(token); + } + + return sb.toString(); + } + + @Override + public LRRPHeader getHeader() + { + if(mHeader == null) + { + mHeader = new LRRPHeader(getMessage(), getOffset()); + } + + return mHeader; + } + + /** + * List of LRRP tokens parsed from the packet + */ + public List getTokens() + { + parseTokens(); + return mTokens; + } + + private void parseTokens() + { + if(mTokens == null) + { + mTokens = new ArrayList<>(); + + int characterCount = getHeader().getPayloadLength(); + int offset = getOffset() + TOKEN_START; + + while(characterCount > 0 && offset < getMessage().size()) + { + String tokenId = getTokenIdentifier(offset); + Token token = TokenFactory.createToken(tokenId, getMessage(), offset, characterCount); + mTokens.add(token); + int length = token.getByteLength(); + characterCount -= token.getByteLength(); + offset += (8 * token.getByteLength()); + } + + Collections.sort(mTokens, (o1, o2) -> o1.getTokenType().compareTo(o2.getTokenType())); + } + } + + /** + * Parses the string hexadecimal value of the 8-bit value that starts at the specified offset. + */ + private String getTokenIdentifier(int offset) + { + int id = getMessage().getInt(BYTE_VALUE, offset); + return String.format("%02X", id); + } + + @Override + public IPacket getPayload() + { + //The payload is a sequence of tokens. There is no packet payload. + return null; + } + + @Override + public List getIdentifiers() + { + return getHeader().getIdentifiers(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Heading.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Heading.java new file mode 100644 index 00000000..807b0512 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Heading.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Heading Token + * + * Start Token: 0x56 + * Total Length: 2 bytes + */ +public class Heading extends Token +{ + private static final int[] HEADING = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Heading(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.HEADING; + } + + /** + * Heading degrees relative to true North + */ + public long getHeading() + { + return getMessage().getInt(HEADING, getOffset()); + } + + @Override + public String toString() + { + return "HEADING:" + getHeading(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Identity.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Identity.java new file mode 100644 index 00000000..8d978201 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Identity.java @@ -0,0 +1,96 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Identity Token + * + * Start Token: 0x22 + * Total Length: 6 bytes + */ +public class Identity extends Token +{ + private static final int IDENTIFIER_START = 16; + private static final int[] BYTE_VALUE = new int[]{0, 1, 2, 3, 4, 5, 6, 7}; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Identity(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.IDENTITY; + } + + private BinaryMessage getTokenContents() + { + return getMessage().getSubMessage(getOffset(), getOffset() + 48); + } + + /** + * Extracts the payload contents. + * + * Note: assumes that the values are 1-4 bytes in big-endian format? Not sure if this is correct because the + * values that I observed only had value in the LSB and the 3 MSBs were all zeros. + */ + public int getID() + { + int length = getVariableLengthPayloadSize(); + int payloadOffset = 16; + int value = 0; + + while(0 < length && length <= 4) + { + int byteValue = getMessage().getInt(BYTE_VALUE, getOffset() + payloadOffset); + + byteValue = Integer.rotateLeft(byteValue, payloadOffset - 16); + value += byteValue; + payloadOffset += 8; + length--; + } + + return value; + } + + + @Override + public String toString() + { + int id = getID(); + + if(id != 0) + { + return "ID:" + id; + } + + return "ID:" + getTokenContents().toHexString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/LRRPPosition.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/LRRPPosition.java new file mode 100644 index 00000000..26d5ddbe --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/LRRPPosition.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.location.LocationIdentifier; +import io.github.dsheirer.identifier.location.Point; +import io.github.dsheirer.protocol.Protocol; + +/** + * LRRP Position/Location + */ +public class LRRPPosition extends LocationIdentifier +{ + /** + * Constructs an instance + * + * @param value location + * @param role to or from + */ + public LRRPPosition(Point value, Role role) + { + super(value, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.LRRP; + } + + /** + * Creates a FROM position + */ + public static LRRPPosition createFrom(double latitude, double longitude) + { + return new LRRPPosition(new Point(latitude, longitude), Role.FROM); + } + + /** + * Creates a TO position + */ + public static LRRPPosition createTo(double latitude, double longitude) + { + return new LRRPPosition(new Point(latitude, longitude), Role.TO); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position.java new file mode 100644 index 00000000..0fd65a25 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position.java @@ -0,0 +1,93 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Position Token + * + * Start Token: 0x66 + * Total Length: 9 bytes + */ +public class Position extends Token +{ + private static final int[] LATITUDE = new int[]{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static final int[] LONGITUDE = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 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 static final double LATITUDE_MULTIPLIER = 180.0 / 4294967295.0d; + private static final double LONGITUDE_MULTIPLIER = 360.0 / 4294967295.0d; + + private LRRPPosition mPosition; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Position(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.POSITION; + } + + /** + * Position value + */ + public LRRPPosition getPosition() + { + if(mPosition == null) + { + mPosition = LRRPPosition.createFrom(getLatitude(), getLongitude()); + } + + return mPosition; + } + + /** + * Latitude in degrees decimal + */ + public double getLatitude() + { + return getMessage().getInt(LATITUDE, getOffset()) * LATITUDE_MULTIPLIER; + } + + /** + * Longitude in degrees decimal + */ + public double getLongitude() + { + return getMessage().getInt(LONGITUDE, getOffset()) * LONGITUDE_MULTIPLIER; + } + + @Override + public String toString() + { + return "POSITION:" + getPosition(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position3D.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position3D.java new file mode 100644 index 00000000..d347ca0f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Position3D.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP 3-D Position with latitude, longitude and altitude + */ +public class Position3D extends Position +{ + private static final int[] ALTITUDE = new int[]{72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87}; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Position3D(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.POSITION_3D; + } + + /** + * Altitude + * + * @return altitude in meters + */ + public int getAltitude() + { + return getMessage().getInt(ALTITUDE, getOffset()); + } + + + @Override + public String toString() + { + return "POSITION:" + getPosition() + " ALT:" + getAltitude() + " MTRS"; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Speed.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Speed.java new file mode 100644 index 00000000..2c96dfde --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Speed.java @@ -0,0 +1,65 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Speed Token + * + * Start Token: 0x6C + * Total Length: 3 bytes + */ +public class Speed extends Token +{ + private static final int[] UNKNOWN = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] SPEED = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Speed(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.SPEED; + } + + /** + * Speed in kilometers per hour (kph) + */ + public long getSpeed() + { + return getMessage().getInt(SPEED, getOffset()); + } + + @Override + public String toString() + { + return "SPEED:" + getSpeed() + " KPH"; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Timestamp.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Timestamp.java new file mode 100644 index 00000000..b3b8811a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Timestamp.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * LRRP Timestamp Token + * + * Start Token: 0x34 + * Total Length: 6 bytes + */ +public class Timestamp extends Token +{ + private static final int[] YEAR = new int[]{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}; + private static final int[] MONTH = new int[]{22, 23, 24, 25}; + private static final int[] DAY = new int[]{26, 27, 28, 29, 30}; + private static final int[] HOUR = new int[]{31, 32, 33, 34, 35}; + private static final int[] MINUTE = new int[]{36, 37, 38, 39, 40, 41}; + private static final int[] SECOND = new int[]{42, 43, 44, 45, 46, 47}; + + /** + * Constructs an instance of a timestamp token. + * + * @param message containing the timestamp + * @param offset to the start of the token + */ + public Timestamp(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.TIMESTAMP; + } + + /** + * Timestamp value in milliseconds + */ + public long getTimestamp() + { + int year = getMessage().getInt(YEAR, getOffset()); + int month = getMessage().getInt(MONTH, getOffset()) - 1; + int day = getMessage().getInt(DAY, getOffset()); + int hour = getMessage().getInt(HOUR, getOffset()); + int minute = getMessage().getInt(MINUTE, getOffset()); + int second = getMessage().getInt(SECOND, getOffset()); + + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.clear(); + calendar.set(year, month, day, hour, minute, second); + return calendar.getTimeInMillis(); + } + + @Override + public String toString() + { + return "TIME:" + new Date(getTimestamp()).toString().toUpperCase(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Token.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Token.java new file mode 100644 index 00000000..403827f8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Token.java @@ -0,0 +1,96 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Token. + * + * LRRP packets are composed of a sequence of tokens that makeup a set of values for the LRRP report. + */ +public abstract class Token +{ + /** + * Length field for a variable length token + */ + private static final int[] VARIABLE_LENGTH_BYTE_COUNT = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + private CorrectedBinaryMessage mMessage; + private int mOffset; + + /** + * Constructs an instance + * + * @param message containing the token and value + * @param offset to the start of the token identifier + */ + public Token(CorrectedBinaryMessage message, int offset) + { + mMessage = message; + mOffset = offset; + } + + /** + * Message that contains this token and value. + */ + protected CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Offset to the start of the token identifier within the message + * + * @return bit offset value + */ + protected int getOffset() + { + return mOffset; + } + + /** + * Type of token and value + */ + public abstract TokenType getTokenType(); + + /** + * Token payload byte length for variable length tokens. + */ + public int getVariableLengthPayloadSize() + { + return getMessage().getInt(VARIABLE_LENGTH_BYTE_COUNT, getOffset()); + } + + /** + * Number of bytes for this token including the token identifier + */ + public int getByteLength() + { + if(getTokenType().isVariableLength()) + { + return getVariableLengthPayloadSize() + 2; //Variable Length Payload + Id + Length Field + } + else + { + return getTokenType().getLength() + 1; //Fixed Length Payload + Id + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenFactory.java new file mode 100644 index 00000000..f199c755 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenFactory.java @@ -0,0 +1,54 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Token Factory + */ +public class TokenFactory +{ + public static Token createToken(String id, CorrectedBinaryMessage message, int offset, int remainingCharacterCount) + { + TokenType tokenType = TokenType.fromValue(id); + + switch(tokenType) + { + case HEADING: + return new Heading(message, offset); + case POSITION: + return new Position(message, offset); + case POSITION_3D: + return new Position3D(message, offset); + case SPEED: + return new Speed(message, offset); + case TIMESTAMP: + return new Timestamp(message, offset); + case IDENTITY: + return new Identity(message, offset); + case UNKNOWN_23: + return new Unknown23(message, offset); + case UNKNOWN: + default: + return new UnknownToken(message, offset, remainingCharacterCount); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenType.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenType.java new file mode 100644 index 00000000..b9dbe389 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/TokenType.java @@ -0,0 +1,105 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import java.util.HashMap; +import java.util.Map; + +/** + * LRRP Token Types + * + * Variable length tokens consist of an identifier, a length indicator, and then a sequence of bytes equal in length + * to the length indicator. + */ +public enum TokenType +{ + POSITION("66", 8), + POSITION_3D("51", 10), + HEADING("56", 1), + SPEED("6C", 2), + TIMESTAMP("34", 5), + IDENTITY("22", -1), + UNKNOWN_23("23", 1), + UNKNOWN("0",0); + + private String mValue; + private int mLength; + + /** + * Constructs an instance + * @param value of the token + * @param length of the token, exclusive of the identifier, where -1 indicates a variable length token. + */ + TokenType(String value, int length) + { + mValue = value; + mLength = length; + } + + /** + * String (Numeric) value of the token + */ + public String getValue() + { + return mValue; + } + + /** + * Indicates if this token is variable length. + */ + public boolean isVariableLength() + { + return mLength == -1; + } + + /** + * Length of the token value in bytes. + * Note: byte count does not include the token byte itself. + */ + public int getLength() + { + return mLength; + } + + private static final Map LOOKUP_MAP = new HashMap<>(); + + static + { + for(TokenType tokenType: TokenType.values()) + { + LOOKUP_MAP.put(tokenType.getValue(), tokenType); + } + } + + /** + * Lookup a token from a 2-character string value + * @param value that represents the token + * @return matching token or UNKNOWN. + */ + public static TokenType fromValue(String value) + { + if(value != null && LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Unknown23.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Unknown23.java new file mode 100644 index 00000000..9d53f7a0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/Unknown23.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * LRRP Unknown Token + * + * Start Token: 0x23 + * Total Length: 2 bytes + */ +public class Unknown23 extends Token +{ + private static final int[] UNKNOWN = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; + + /** + * Constructs an instance of a heading token. + * + * @param message containing the heading + * @param offset to the start of the token + */ + public Unknown23(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public TokenType getTokenType() + { + return TokenType.UNKNOWN_23; + } + + private BinaryMessage getTokenContents() + { + return getMessage().getSubMessage(getOffset(), getOffset() + 16); + } + + @Override + public String toString() + { + return "UNK:" + getTokenContents().toHexString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/UnknownToken.java b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/UnknownToken.java new file mode 100644 index 00000000..f4a088d6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/lrrp/token/UnknownToken.java @@ -0,0 +1,61 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.lrrp.token; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Unknown or Unrecognized token. This class is also used to capture the remaining character content of the token + * string when parsing tokens from a LRRP packet, to account for the remaining token content. + */ +public class UnknownToken extends Token +{ + private int mCharacterLength; + + /** + * Constructs an instance + * + * @param message containing the token and value + * @param offset to the start of the token identifier + */ + public UnknownToken(CorrectedBinaryMessage message, int offset, int characterLength) + { + super(message, offset); + mCharacterLength = characterLength; + } + + @Override + public TokenType getTokenType() + { + return TokenType.UNKNOWN; + } + + private BinaryMessage getTokenContents() + { + return getMessage().getSubMessage(getOffset(), getOffset() + (mCharacterLength * 8)); + } + + @Override + public String toString() + { + return "UNK:" + getTokenContents().toHexString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPHeader.java index 98376267..24bbbb11 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPHeader.java @@ -28,6 +28,9 @@ public class UDPHeader extends Header private static final int[] SOURCE_PORT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] DESTINATION_PORT = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + private UDPPort mSourcePort; + private UDPPort mDestinationPort; + /** * Constructs a parser for a header contained within a binary message starting at the offset. * @@ -63,16 +66,26 @@ public class UDPHeader extends Header * Source UDP port number * @return */ - public int getSourcePort() + public UDPPort getSourcePort() { - return getMessage().getInt(SOURCE_PORT, getOffset()); + if(mSourcePort == null) + { + mSourcePort = UDPPort.createFrom(getMessage().getInt(SOURCE_PORT, getOffset())); + } + + return mSourcePort; } /** * Destination UDP port number */ - public int getDestinationPort() + public UDPPort getDestinationPort() { - return getMessage().getInt(DESTINATION_PORT, getOffset()); + if(mDestinationPort == null) + { + mDestinationPort = UDPPort.createTo(getMessage().getInt(DESTINATION_PORT, getOffset())); + } + + return mDestinationPort; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPacket.java index af572839..ea7a507f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPacket.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPacket.java @@ -20,20 +20,21 @@ package io.github.dsheirer.module.decode.ip.udp; -import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.Packet; import io.github.dsheirer.module.decode.ip.PacketMessageFactory; import io.github.dsheirer.module.decode.ip.UnknownPacket; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; public class UDPPacket extends Packet { private UDPHeader mHeader; private IPacket mPayload; + private List mIdentifiers; /** * Constructs a parser for a header contained within a binary message starting at the offset. @@ -41,7 +42,7 @@ public class UDPPacket extends Packet * @param message containing the packet * @param offset to the packet within the message */ - public UDPPacket(BinaryMessage message, int offset) + public UDPPacket(CorrectedBinaryMessage message, int offset) { super(message, offset); } @@ -77,7 +78,7 @@ public class UDPPacket extends Packet @Override public IPacket getPayload() { - if(mPayload == null) + if(mPayload == null && getHeader().isValid()) { if(getHeader().isValid()) { @@ -97,11 +98,19 @@ public class UDPPacket extends Packet @Override public List getIdentifiers() { - if(hasPayload()) + if(mIdentifiers == null) { - return getPayload().getIdentifiers(); + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getHeader().getSourcePort()); + mIdentifiers.add(getHeader().getDestinationPort()); + + //Roll up the identifiers from the payload packet + if(hasPayload()) + { + mIdentifiers.addAll(getPayload().getIdentifiers()); + } } - return Collections.emptyList(); + return mIdentifiers; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPort.java b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPort.java new file mode 100644 index 00000000..4c0b6cbd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/udp/UDPPort.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.udp; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * UDP Port Number Identifier + */ +public class UDPPort extends IntegerIdentifier +{ + /** + * Constructs an instance + * @param value of the port + * @param role of the port (TO/FROM) + */ + public UDPPort(int value, Role role) + { + super(value, IdentifierClass.NETWORK, Form.UDP_PORT, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.UDP; + } + + /** + * Utility method to create a FROM port + */ + public static UDPPort createFrom(int value) + { + return new UDPPort(value, Role.FROM); + } + + /** + * Utility method to create a TO port + */ + public static UDPPort createTo(int value) + { + return new UDPPort(value, Role.TO); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/NetworkFrequencyFile.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/NetworkFrequencyFile.java new file mode 100644 index 00000000..35cb4174 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/NetworkFrequencyFile.java @@ -0,0 +1,170 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import com.google.common.base.Joiner; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork; +import io.github.dsheirer.module.decode.ip.IHeader; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.Packet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * XCMP Transfer: Network Frequency File + */ +public class NetworkFrequencyFile extends Packet +{ + private static final int[] VERSION = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + private static final int[] SEGMENT_NUMBER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + private static final int[] SEGMENT_COUNT = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] NETWORK = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}; + private static final int[] UNKNOWN = new int[]{60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, + 77, 78, 79}; + private static final int REPEATER_STRUCTURE_START = 80; + private static final int REPEATER_STRUCTURE_LENGTH = 72; + private List mRepeaters; + private DMRNetwork mNetwork; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the packet + * @param offset to the packet within the message + */ + public NetworkFrequencyFile(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("NETWORK FREQUENCY FILE VERSION:").append(getVersion()); + sb.append(" NETWORK:").append(getNetwork()); + sb.append(" SEGMENT:").append(getSegmentNumber()).append(" OF ").append(getSegmentCount()); + + List repeaters = getRepeaters(); + + if(repeaters.isEmpty()) + { + sb.append(" NO REPEATER INFO"); + } + else + { + sb.append(" ").append(Joiner.on(" | ").join(mRepeaters)); + } + + return sb.toString(); + } + + public DMRNetwork getNetwork() + { + if(mNetwork == null) + { + mNetwork = DMRNetwork.create(getMessage().getInt(NETWORK, getOffset())); + } + + return mNetwork; + } + + /** + * Unknown 20-bit field contents + */ + public String getUnknown() + { + return String.format("%05X", getMessage().getInt(UNKNOWN, getOffset())); + } + + /** + * Segment number for this packet + * + * @return + */ + public int getSegmentNumber() + { + return getMessage().getInt(SEGMENT_NUMBER, getOffset()); + } + + /** + * Number of segments contained in the complete file + */ + public int getSegmentCount() + { + return getMessage().getInt(SEGMENT_COUNT, getOffset()); + } + + /** + * List of repeater structures parsed from the Network Information File segment + */ + public List getRepeaters() + { + if(mRepeaters == null) + { + mRepeaters = new ArrayList<>(); + + int offset = getOffset() + REPEATER_STRUCTURE_START; + + for(int x = 0; x < 15; x++) + { + if(offset <= getMessage().length()) + { + Repeater repeater = new Repeater(getMessage(), offset, getNetwork()); + + if(repeater.isValid()) + { + mRepeaters.add(repeater); + } + + offset += REPEATER_STRUCTURE_LENGTH; + } + } + } + + return mRepeaters; + } + + public int getVersion() + { + return getMessage().getInt(VERSION, getOffset()); + } + + @Override + public IHeader getHeader() + { + return null; + } + + @Override + public IPacket getPayload() + { + return null; + } + + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/Repeater.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/Repeater.java new file mode 100644 index 00000000..a01f6f61 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/Repeater.java @@ -0,0 +1,157 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork; +import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; + +/** + * Repeater Structure Parser + */ +public class Repeater +{ + private static int[] SITE = new int[]{0, 1, 2, 3, 4, 5, 6, 7}; + private static int[] REPEATER_ID = new int[]{8, 9, 10, 11}; + private static int CONTROL_CHANNEL_FLAG = 12; + private static int[] UPLINK_FREQUENCY = new int[]{13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; + private static int[] COLOR_CODE = new int[]{40, 41, 42, 43}; + private static int RESERVED = 44; + private static int[] DOWNLINK_FREQUENCY = new int[]{45, 46, 47, 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 CorrectedBinaryMessage mMessage; + private int mOffset; + private DMRNetwork mDMRNetwork; + private DMRSite mSite; + + /** + * Constructs an instance + * + * @param message containing a repeater structure + * @param offset into the message to where the repeater structure starts + */ + public Repeater(CorrectedBinaryMessage message, int offset, DMRNetwork dmrNetwork) + { + mMessage = message; + mOffset = offset; + mDMRNetwork = dmrNetwork; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("REPEATER:").append(getNetwork()).append(".").append(getSite()).append(".").append(getRepeaterNumber()); + sb.append(" CC:").append(getColorCode()); + sb.append(" DN:").append(getDownlinkFrequency() / 1E6d); + sb.append(" UP:").append(getUplinkFrequency() / 1E6d); + if(isControlChannel()) + { + sb.append(" CONTROL"); + } + return sb.toString(); + } + + public DMRNetwork getNetwork() + { + return mDMRNetwork; + } + + /** + * Message containing this repeater structure + */ + private CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + /** + * Offset into the message where this structure starts + */ + private int getOffset() + { + return mOffset; + } + + /** + * Indicates if this repeater structure contains valid information + * @return true if valid, or false if the downlink and uplink frequency values are the same. + */ + public boolean isValid() + { + return getDownlinkFrequency() != getUplinkFrequency(); + } + + /** + * Color code for the repeater + */ + public int getColorCode() + { + return getMessage().getInt(COLOR_CODE, getOffset()); + } + + /** + * Repeater receive / uplink frequency + * @return frequency in Hertz + */ + public long getUplinkFrequency() + { + return getMessage().getInt(UPLINK_FREQUENCY, getOffset()) * 10l; + } + + /** + * Repeater transmit / downlink frequency + * @return frequency in Hertz + */ + public long getDownlinkFrequency() + { + return getMessage().getInt(DOWNLINK_FREQUENCY, getOffset()) * 10l; + } + + /** + * Indicates if this is a control channel + */ + public boolean isControlChannel() + { + return getMessage().get(CONTROL_CHANNEL_FLAG + getOffset()); + } + + /** + * Repeater Number/ID + * + * @return id as a 1-based index + */ + public int getRepeaterNumber() + { + return getMessage().getInt(REPEATER_ID, getOffset()) + 1; + } + + public DMRSite getSite() + { + if(mSite == null) + { + mSite = DMRSite.create(getMessage().getInt(SITE, getOffset())); + } + + return mSite; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPHeader.java new file mode 100644 index 00000000..0b0b64c1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPHeader.java @@ -0,0 +1,110 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.Header; + +import java.util.Collections; +import java.util.List; + +/** + * EXtensible Command Message Protocol (XCMP) Header + */ +public class XCMPHeader extends Header +{ + private static final int[] MESSAGE_TYPE = {0, 1, 2, 3, 4, 5, 6, 7}; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the header + * @param offset to the header within the message + */ + public XCMPHeader(BinaryMessage message, int offset) + { + super(message, offset); + } + + /** + * Identify the XCMP message type from the message + */ + public static XCMPMessageType getMessageType(BinaryMessage message, int offset) + { + return XCMPMessageType.fromValue(message.getInt(MESSAGE_TYPE, offset)); + } + + /** + * Identify the XCMP message type for this message + */ + public XCMPMessageType getMessageType() + { + return getMessageType(getMessage(), getOffset()); + } + + /** + * Message Type Value + */ + public int getMessageTypeValue() + { + return getMessage().getInt(MESSAGE_TYPE, getOffset()); + } + + /** + * Length of this header in bits + * + * Note this length value includes the first 2 bytes of the message that indicates the overall + * message length and the length of the PDU header byte(s). + * + * This method may need to be overridden if there is more than 1 extension header in a PDU. + */ + @Override + public int getLength() + { + return 8; + } + + /** + * Identifiers for this packet + */ + public List getIdentifiers() + { + return Collections.emptyList(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("XCMP"); + + if(getMessageType() != XCMPMessageType.UNKNOWN) + { + sb.append(" ").append(getMessageType()); + } + else + { + sb.append(" UNKNOWN FILE TYPE:").append(String.format("%02X", getMessageTypeValue())); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPMessageType.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPMessageType.java new file mode 100644 index 00000000..03defd80 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPMessageType.java @@ -0,0 +1,78 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import java.util.Map; +import java.util.TreeMap; + +/** + * XCMP Message Types enumeration + */ +public enum XCMPMessageType +{ + NETWORK_FREQUENCY_FILE(2, "NETWORK FREQUENCY FILE"), + UNKNOWN(-1, "UNKNOWN"); + + private int mValue; + private String mLabel; + + XCMPMessageType(int value, String label) + { + mValue = value; + mLabel = label; + } + + /** + * Numeric value for the type + */ + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return mLabel; + } + + private static final Map LOOKUP_MAP = new TreeMap<>(); + + static + { + for(XCMPMessageType type: XCMPMessageType.values()) + { + LOOKUP_MAP.put(type.getValue(), type); + } + } + + /** + * Lookup the XCMP Message type from the value + */ + public static XCMPMessageType fromValue(int value) + { + if(LOOKUP_MAP.containsKey(value)) + { + return LOOKUP_MAP.get(value); + } + + return UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacket.java new file mode 100644 index 00000000..8c8fce99 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacket.java @@ -0,0 +1,85 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.Packet; + +import java.util.List; + +/** + * EXtensible Command Message Protocol (XCMP) Packet + */ +public class XCMPPacket extends Packet +{ + private XCMPHeader mHeader; + private IPacket mPayload; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the packet + * @param offset to the packet within the message + */ + public XCMPPacket(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(getHeader()); + sb.append(" ").append(getPayload()); + return sb.toString(); + } + + @Override + public XCMPHeader getHeader() + { + if(mHeader == null) + { + mHeader = new XCMPHeader(getMessage(), getOffset()); + } + + return mHeader; + } + + @Override + public IPacket getPayload() + { + if(mPayload == null) + { + mPayload = XCMPPacketFactory.create(getMessage(), getOffset() + getHeader().getLength(), + getHeader().getMessageType()); + } + + return mPayload; + } + + @Override + public List getIdentifiers() + { + return getHeader().getIdentifiers(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacketFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacketFactory.java new file mode 100644 index 00000000..1a890a91 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/xcmp/XCMPPacketFactory.java @@ -0,0 +1,45 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.xcmp; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.UnknownPacket; + +public class XCMPPacketFactory +{ + /** + * Creates an XCMP Packet payload + * @param message containing the full packet payload + * @param offset to the start of the XCMP message + * @return XCMP packet parser + */ + public static IPacket create(CorrectedBinaryMessage message, int offset, XCMPMessageType messageType) + { + switch(messageType) + { + case NETWORK_FREQUENCY_FILE: + return new NetworkFrequencyFile(message, offset); + case UNKNOWN: + default: + return new UnknownPacket(message, offset); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java index a942b6aa..e8243c42 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java @@ -69,13 +69,6 @@ public class LJ1200Decoder extends AbstractAFSKDecoder implements IBinarySymbolP return DecoderType.LJ_1200; } - public void dispose() - { - super.dispose(); - mTowerMessageFramer.dispose(); - mMessageProcessor.dispose(); - } - @Override public void process(boolean symbol) { diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java index 65c22349..9763f463 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200DecoderState.java @@ -132,7 +132,7 @@ public class LJ1200DecoderState extends DecoderState @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { - if(event.getEvent() == Event.RESET) + if(event.getEvent() == Event.REQUEST_RESET) { resetState(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java index 34c82147..c29402ae 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoderState.java @@ -664,7 +664,7 @@ public class LTRNetDecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/LtrSiteIdentifier.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/LtrSiteIdentifier.java new file mode 100644 index 00000000..fecb8356 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/LtrSiteIdentifier.java @@ -0,0 +1,43 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2018 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 + * ***************************************************************************** + */ + +package io.github.dsheirer.module.decode.ltrnet.identifier; + +import io.github.dsheirer.identifier.site.SiteIdentifier; +import io.github.dsheirer.protocol.Protocol; + +public class LtrSiteIdentifier extends SiteIdentifier +{ + public LtrSiteIdentifier(int site) + { + super(site); + } + + @Override + public Protocol getProtocol() + { + return Protocol.LTR_NET; + } + + public static LtrSiteIdentifier create(int site) + { + return new LtrSiteIdentifier(site); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/message/osw/SiteId.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/message/osw/SiteId.java index eca264ba..4f6e47de 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/message/osw/SiteId.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/message/osw/SiteId.java @@ -22,7 +22,7 @@ package io.github.dsheirer.module.decode.ltrnet.message.osw; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ltrnet.LtrNetMessageType; -import io.github.dsheirer.module.decode.ltrnet.identifier.SiteIdentifier; +import io.github.dsheirer.module.decode.ltrnet.identifier.LtrSiteIdentifier; import java.util.ArrayList; import java.util.List; @@ -32,7 +32,7 @@ import java.util.List; */ public class SiteId extends LtrNetOswMessage { - private SiteIdentifier mSiteIdentifier; + private LtrSiteIdentifier mLtrSiteIdentifier; private List mIdentifiers; /** @@ -57,14 +57,14 @@ public class SiteId extends LtrNetOswMessage return sb.toString(); } - public SiteIdentifier getSiteID() + public LtrSiteIdentifier getSiteID() { - if(mSiteIdentifier == null) + if(mLtrSiteIdentifier == null) { - mSiteIdentifier = SiteIdentifier.create(getMessage().getInt(23, 32)); + mLtrSiteIdentifier = LtrSiteIdentifier.create(getMessage().getInt(23, 32)); } - return mSiteIdentifier; + return mLtrSiteIdentifier; } diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java index 8a63767e..2be9d1fc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoderState.java @@ -222,7 +222,7 @@ public class LTRStandardDecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; } diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java index 31c8eb66..e73ae4e1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java @@ -60,14 +60,6 @@ public class MDCDecoder extends AbstractAFSKDecoder mMessageProcessor.addMessageListener(getMessageListener()); } - public void dispose() - { - super.dispose(); - mNRZDecoder.dispose(); - mMessageFramer.dispose(); - mMessageProcessor.dispose(); - } - @Override public DecoderType getDecoderType() { diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java index 7875de83..f79fefd8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoderState.java @@ -188,7 +188,7 @@ public class MDCDecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java index b835f180..b9364e9f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java @@ -103,18 +103,6 @@ public class MPT1327Decoder extends AbstractAFSKDecoder implements IBinarySymbol mTrafficMessageFramer.reset(); } - /** - * Cleanup method - */ - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mControlMessageFramer.dispose(); - mTrafficMessageFramer.dispose(); - } - public MessageFramer getControlMessageFramer() { return mControlMessageFramer; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java index 064bfc4f..4e4cd50a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327DecoderState.java @@ -95,11 +95,6 @@ public class MPT1327DecoderState extends DecoderState return mChannelType; } - public void dispose() - { - super.dispose(); - } - @Override public void receive(IMessage message) { @@ -431,10 +426,10 @@ public class MPT1327DecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; - case SOURCE_FREQUENCY: + case NOTIFICATION_SOURCE_FREQUENCY: mFrequency = event.getFrequency(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Message.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Message.java index 687cb88e..87abb4d2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Message.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Message.java @@ -1301,7 +1301,7 @@ public class MPT1327Message extends Message public String getSDMTelex(MPTMessageType type) { - ArrayList values = new ArrayList(); + ArrayList values = new ArrayList<>(); int blocks = 0; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327TrafficChannelManager.java index 1c01fdc5..af6fcef3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327TrafficChannelManager.java @@ -22,19 +22,19 @@ package io.github.dsheirer.module.decode.mpt1327; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.ChannelEvent; -import io.github.dsheirer.controller.channel.ChannelGrantEvent; import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventProvider; +import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.module.Module; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.IDecodeEvent; import io.github.dsheirer.module.decode.event.IDecodeEventProvider; import io.github.dsheirer.module.decode.mpt1327.channel.MPT1327Channel; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.config.SourceConfigTuner; import org.slf4j.Logger; @@ -49,8 +49,8 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; -public class MPT1327TrafficChannelManager extends Module implements IDecodeEventProvider, IChannelEventListener, - IChannelEventProvider +public class MPT1327TrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, + IChannelEventListener, IChannelEventProvider { private final static Logger mLog = LoggerFactory.getLogger(MPT1327TrafficChannelManager.class); @@ -133,7 +133,8 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent sourceConfig.setFrequency(mpt1327Channel.getDownlinkFrequency()); trafficChannel.setSourceConfiguration(sourceConfig); mAllocatedTrafficChannelMap.put(mpt1327Channel, trafficChannel); - broadcast(new ChannelGrantEvent(trafficChannel, ChannelEvent.Event.REQUEST_ENABLE, mpt1327Channel, identifierCollection)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, mpt1327Channel, + identifierCollection)); } broadcast(channelGrantEvent); @@ -241,6 +242,7 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent @Override public void stop() { + mAvailableTrafficChannelQueue.clear(); List channels = new ArrayList<>(mAllocatedTrafficChannelMap.values()); //Issue a disable request for each traffic channel @@ -284,18 +286,6 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent return null; } - @Override - public void dispose() - { - for(Channel trafficChannel : mAvailableTrafficChannelQueue) - { - broadcast(new ChannelEvent(trafficChannel, ChannelEvent.Event.REQUEST_DISABLE)); - } - - mAvailableTrafficChannelQueue.clear(); - } - - @Override public void addDecodeEventListener(Listener listener) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 10c68ae1..c6ba79bb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -25,9 +25,9 @@ import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.Channel.ChannelType; import io.github.dsheirer.controller.channel.ChannelEvent; import io.github.dsheirer.controller.channel.ChannelEvent.Event; -import io.github.dsheirer.controller.channel.ChannelGrantEvent; import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventProvider; +import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection; @@ -35,7 +35,6 @@ import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.scramble.ScrambleParameterIdentifier; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageListener; -import io.github.dsheirer.module.Module; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.event.DecodeEvent; @@ -54,6 +53,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.Net import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; import io.github.dsheirer.module.decode.p25.reference.ServiceOptions; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.config.SourceConfigTuner; import org.slf4j.Logger; @@ -85,7 +85,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; * * This manager monitors channel events watching for events related to managed traffic channels. */ -public class P25TrafficChannelManager extends Module implements IDecodeEventProvider, IChannelEventListener, +public class P25TrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener, IChannelEventProvider, IMessageListener { private final static Logger mLog = LoggerFactory.getLogger(P25TrafficChannelManager.class); @@ -315,7 +315,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv sourceConfig.setFrequency(frequency); trafficChannel.setSourceConfiguration(sourceConfig); mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection)); } } @@ -361,7 +361,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv sourceConfig.setFrequency(frequency); trafficChannel.setSourceConfiguration(sourceConfig); mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection)); } broadcast(channelGrantEvent); @@ -465,7 +465,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv decodeConfig.setScrambleParameters(mPhase2ScrambleParameters.copy()); } - broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection)); } } @@ -519,7 +519,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv sourceConfig.setFrequency(frequency); trafficChannel.setSourceConfiguration(sourceConfig); mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection)); } broadcast(channelGrantEvent); @@ -646,20 +646,6 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv return null; } - @Override - public void dispose() - { - for(Channel trafficChannel : mAvailablePhase1TrafficChannelQueue) - { - broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE)); - } - - for(Channel trafficChannel : mAvailablePhase2TrafficChannelQueue) - { - broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE)); - } - } - /** * Implements the IDecodeEventProvider interface to provide channel events to an external listener. */ @@ -691,6 +677,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv @Override public void stop() { + mAvailablePhase1TrafficChannelQueue.clear(); + mAvailablePhase2TrafficChannelQueue.clear(); + List channels = new ArrayList<>(mAllocatedTrafficChannelMap.values()); //Issue a disable request for each traffic channel diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java index df92d82f..a1aae512 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java @@ -55,21 +55,13 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat private Listener mIdentifierUpdateNotificationListener; private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private ToneMetadataProcessor mToneMetadataProcessor = new ToneMetadataProcessor(); - private int mTimeslot; private Queue mQueuedAudioTimeslots = new ArrayDeque<>(); private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCall = false; public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList) { - super(userPreferences, aliasList); - mTimeslot = timeslot; - getIdentifierCollection().setTimeslot(timeslot); - } - - protected int getTimeslot() - { - return mTimeslot; + super(userPreferences, aliasList, timeslot); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java index 620e8dcc..6c2d6ef5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java @@ -122,14 +122,6 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve return (float)(getSampleRate() / getSymbolRate()); } - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mMessageProcessor = null; - } - @Override public Listener getSourceEventListener() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java index e4b5efa2..6f1a317c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java @@ -165,16 +165,6 @@ public class P25P1DecoderC4FM extends P25P1Decoder return filter; } - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mMessageFramer = null; - } - @Override protected void process(SourceEvent sourceEvent) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java index 0afc9a93..171f579e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java @@ -168,22 +168,6 @@ public class P25P1DecoderLSM extends P25P1Decoder mFrequencyCorrectionSyncMonitor.reset(); } - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mAGC.dispose(); - mAGC = null; - - mQPSKDemodulator.dispose(); - mQPSKDemodulator = null; - - mMessageFramer = null; - } - public Modulation getModulation() { return Modulation.CQPSK; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java index aa9725bd..293423b1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java @@ -2113,7 +2113,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); mNetworkConfigurationMonitor.reset(); break; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java index 70269382..d88f488d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java @@ -44,7 +44,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.P25Message; import io.github.dsheirer.module.decode.p25.phase1.message.P25MessageFactory; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUMessageFactory; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence; -import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessageFactory; import io.github.dsheirer.preference.UserPreferences; @@ -450,7 +449,7 @@ public class P25P1MessageFramer implements Listener, IP25P1DataUnitDetect public static void main(String[] args) { - Path directory = Paths.get("/home/denny/Documents/TMR/APCO25/GPS/cnyicc_cellocator"); + Path directory = Paths.get("/media/denny/500G1EXT4/RadioRecordings/APCO25/Ken Noffsinger"); // Path directory = Paths.get("/media/denny/500G1EXT4/PBITRecordings"); UserPreferences userPreferences = new UserPreferences(); @@ -464,7 +463,6 @@ public class P25P1MessageFramer implements Listener, IP25P1DataUnitDetect AudioRecordingManager recordingManager = new AudioRecordingManager(userPreferences); recordingManager.start(); ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel()); - processingChain.addAudioSegmentListener(recordingManager); processingChain.addModule(new P25P1DecoderState(channel)); processingChain.addModule(new P25P1AudioModule(userPreferences, aliasList)); @@ -515,7 +513,7 @@ public class P25P1MessageFramer implements Listener, IP25P1DataUnitDetect mLog.error("Error", ioe); } - if(message instanceof PacketMessage) + if(!(message instanceof SyncLossMessage)) { mLog.debug(message.toString()); } @@ -548,5 +546,7 @@ public class P25P1MessageFramer implements Listener, IP25P1DataUnitDetect { mLog.error("Error", ioe); } + + mLog.info("Finished!"); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java index 889b1ccc..bae93fe8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HDUMessage.java @@ -24,7 +24,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.hdu; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.edac.Golay18; -import io.github.dsheirer.edac.ReedSolomon_63_47_17; +import io.github.dsheirer.edac.ReedSolomon_63_47_17_P25; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID; import io.github.dsheirer.module.decode.p25.phase1.message.P25Message; @@ -77,8 +77,7 @@ public class HDUMessage extends P25Message private static final int[] RS_HEX_14 = {612, 613, 614, 615, 616, 617}; private static final int[] RS_HEX_15 = {630, 631, 632, 633, 634, 635}; - //Reed-Solomon(36,20,17) code protects the header word. Maximum correctable errors are: 8 - private static final ReedSolomon_63_47_17 reedSolomon_63_47_17 = new ReedSolomon_63_47_17(8); + private static final ReedSolomon_63_47_17_P25 reedSolomon_63_47_17 = new ReedSolomon_63_47_17_P25(); private HeaderData mHeaderData; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU1Message.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU1Message.java index 5ebaf524..524c1f67 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU1Message.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU1Message.java @@ -24,7 +24,7 @@ import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.edac.Hamming10; -import io.github.dsheirer.edac.ReedSolomon_63_47_17; +import io.github.dsheirer.edac.ReedSolomon_24_12_13_P25; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver; @@ -89,8 +89,8 @@ public class LDU1Message extends LDUMessage implements IFrequencyBandReceiver private static final int[] RS_HEX_10 = {1228, 1229, 1230, 1231, 1232, 1233}; private static final int[] RS_HEX_11 = {1238, 1239, 1240, 1241, 1242, 1243}; - //Reed-Solomon(24,12,13) code protects the link control word. Maximum correctable errors are: 6 - private static final ReedSolomon_63_47_17 reedSolomon_63_47_17 = new ReedSolomon_63_47_17(6); + //Reed-Solomon(24,12,13) code protects the link control word. Maximum correctable errors are: floor(13/2) = 6 + private static final ReedSolomon_24_12_13_P25 REED_SOLOMON_24_12_13_P25 = new ReedSolomon_24_12_13_P25(); private LinkControlWord mLinkControlWord; private List mIdentifiers; @@ -177,7 +177,7 @@ public class LDU1Message extends LDUMessage implements IFrequencyBandReceiver input[23] = getMessage().getInt(CW_HEX_0); /* indexes 24 - 62 are defaulted to zero */ - boolean irrecoverableErrors = reedSolomon_63_47_17.decode(input, output); + boolean irrecoverableErrors = REED_SOLOMON_24_12_13_P25.decode(input, output); //Transfer error corrected output to a new binary message BinaryMessage binaryMessage = new BinaryMessage(72); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU2Message.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU2Message.java index 311d0c60..56d5c59c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU2Message.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/LDU2Message.java @@ -23,7 +23,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.ldu; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.edac.Hamming10; -import io.github.dsheirer.edac.ReedSolomon_63_47_17; +import io.github.dsheirer.edac.ReedSolomon_24_16_9_P25; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID; import org.slf4j.Logger; @@ -63,8 +63,8 @@ public class LDU2Message extends LDUMessage private static final int[] RS_HEX_6 = {1228, 1229, 1230, 1231, 1232, 1233}; private static final int[] RS_HEX_7 = {1238, 1239, 1240, 1241, 1242, 1243}; - //Reed-Solomon(24,16,9) code protects the encryption sync word. Maximum correctable errors are: 4 - private static final ReedSolomon_63_47_17 reedSolomon_63_47_17 = new ReedSolomon_63_47_17(4); + //Reed-Solomon(24,16,9) code protects the encryption sync word. Maximum correctable errors are floor(9/2) = 4 + private static final ReedSolomon_24_16_9_P25 REED_SOLOMON_24_16_9_P25 = new ReedSolomon_24_16_9_P25(); private EncryptionSyncParameters mEncryptionSyncParameters; private List mIdentifiers; @@ -140,7 +140,7 @@ public class LDU2Message extends LDUMessage input[23] = getMessage().getInt(CW_HEX_0); /* indexes 24 - 62 are defaulted to zero */ - boolean irrecoverableErrors = reedSolomon_63_47_17.decode(input, output); + boolean irrecoverableErrors = REED_SOLOMON_24_16_9_P25.decode(input, output); BinaryMessage binaryMessage = new BinaryMessage(96); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/packet/PacketMessage.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/packet/PacketMessage.java index 8b86d21c..6acf8967 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/packet/PacketMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/packet/PacketMessage.java @@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet; import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.PacketMessageFactory; @@ -46,7 +47,7 @@ public class PacketMessage extends P25Message private PDUSequence mPDUSequence; private BinaryMessage mPayload; - private BinaryMessage mPacketMessage; + private CorrectedBinaryMessage mPacketMessage; private SNDCPPacketHeader mSNDCPPacketHeader; private IPacket mPacket; private List mIdentifiers; @@ -185,7 +186,7 @@ public class PacketMessage extends P25Message return mPacket; } - public BinaryMessage getPacketMessage() + public CorrectedBinaryMessage getPacketMessage() { if(mPacketMessage == null) { @@ -197,13 +198,14 @@ public class PacketMessage extends P25Message end -= (getHeader().getPadOctetCount() * 8); if(end > start) { - mPacketMessage = getPayloadMessage().getSubMessage(start, end); + BinaryMessage message = getPayloadMessage().getSubMessage(start, end); + mPacketMessage = new CorrectedBinaryMessage(message); } } if(mPacketMessage == null) { - mPacketMessage = new BinaryMessage(0); + mPacketMessage = new CorrectedBinaryMessage(0); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java index efcf04d6..0645fe9a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java @@ -23,7 +23,7 @@ import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.edac.Golay24; -import io.github.dsheirer.edac.ReedSolomon_63_47_17; +import io.github.dsheirer.edac.ReedSolomon_24_12_13_P25; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver; @@ -62,7 +62,7 @@ public class TDULinkControlMessage extends P25Message implements IFrequencyBandR public static final int[] RS_HEX_11 = {270, 271, 272, 273, 274, 275}; //Reed-Solomon(24,12,13) code protects the link control word. Maximum correctable errors are: 6 - private static final ReedSolomon_63_47_17 reedSolomon_63_47_17 = new ReedSolomon_63_47_17(6); + private static final ReedSolomon_24_12_13_P25 REED_SOLOMON_24_12_13_P25 = new ReedSolomon_24_12_13_P25(); private LinkControlWord mLinkControlWord; @@ -146,7 +146,7 @@ public class TDULinkControlMessage extends P25Message implements IFrequencyBandR input[23] = getMessage().getInt(LC_HEX_0); /* indexes 24 - 62 are defaulted to zero */ - boolean irrecoverableErrors = reedSolomon_63_47_17.decode(input, output); + boolean irrecoverableErrors = REED_SOLOMON_24_12_13_P25.decode(input, output); //Transfer error corrected output to a new binary message BinaryMessage binaryMessage = new BinaryMessage(72); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/DecodeConfigP25Phase2.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/DecodeConfigP25Phase2.java index 0535125d..522d7617 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/DecodeConfigP25Phase2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/DecodeConfigP25Phase2.java @@ -60,6 +60,15 @@ public class DecodeConfigP25Phase2 extends DecodeConfiguration return 2; } + @Override + public int[] getTimeslots() + { + int[] timeslots = new int[2]; + timeslots[0] = 0; + timeslots[1] = 1; + return timeslots; + } + /** * Optional scramble (ie randomizer) parameters to use for the channel. */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java index 549822a9..ed74cb3f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java @@ -124,14 +124,6 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve return (float)(getSampleRate() / getSymbolRate()); } - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mMessageProcessor = null; - } - @Override public Listener getSourceEventListener() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java index 0949ca48..dcb4555e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java @@ -182,16 +182,6 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate return filter; } - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mMessageFramer = null; - } - @Override protected void process(SourceEvent sourceEvent) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java index d3b4acbe..6e020afb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java @@ -1229,7 +1229,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); mNetworkConfigurationMonitor.reset(); break; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java index 9fb9a5fa..8a1dbdf9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java @@ -18,13 +18,8 @@ */ package io.github.dsheirer.module.decode.p25.phase2; -import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.alias.id.record.Record; -import io.github.dsheirer.alias.id.talkgroup.Talkgroup; -import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange; -import io.github.dsheirer.audio.playback.AudioPlaybackManager; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; @@ -33,26 +28,17 @@ import io.github.dsheirer.dsp.symbol.ISyncDetectListener; import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.MessageProviderModule; -import io.github.dsheirer.message.StuffBitsMessage; -import io.github.dsheirer.module.Module; import io.github.dsheirer.module.ProcessingChain; -import io.github.dsheirer.module.decode.DecoderFactory; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule; -import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer; -import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; import io.github.dsheirer.preference.UserPreferences; -import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.record.AudioRecordingManager; import io.github.dsheirer.record.binary.BinaryReader; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableByteBuffer; -import io.github.dsheirer.settings.SettingsManager; -import io.github.dsheirer.source.SourceManager; -import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,7 +48,6 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.function.Consumer; /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java index 3f708288..cf509c52 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/EncryptionSynchronizationSequenceProcessor.java @@ -23,7 +23,7 @@ package io.github.dsheirer.module.decode.p25.phase2.message; import io.github.dsheirer.bits.BinaryMessage; -import io.github.dsheirer.edac.ReedSolomon_44_16_29; +import io.github.dsheirer.edac.ReedSolomon_44_16_29_P25; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice4Timeslot; @@ -177,7 +177,7 @@ public class EncryptionSynchronizationSequenceProcessor int[] output = new int[63]; - ReedSolomon_44_16_29 rs = new ReedSolomon_44_16_29(); + ReedSolomon_44_16_29_P25 rs = new ReedSolomon_44_16_29_P25(); boolean irrecoverableErrors = rs.decode(input, output); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java index 29f99ca4..a959d08b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/FacchTimeslot.java @@ -24,7 +24,7 @@ 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.edac.ReedSolomon_63_35_29; +import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; @@ -204,8 +204,7 @@ public class FacchTimeslot extends AbstractSignalingTimeslot // input[61] = 0; //Shortened // input[62] = 0; //Shortened - //Reed-Solomon(45,26,20) code protects the SOEMI word. Maximum correctable errors are: 13 (53 - 26 / 2) - ReedSolomon_63_35_29 reedSolomon_63_35_29 = new ReedSolomon_63_35_29(13); + ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25(); boolean irrecoverableErrors; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java index f707e613..45217ea9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/SacchTimeslot.java @@ -24,7 +24,7 @@ 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.edac.ReedSolomon_63_35_29; +import io.github.dsheirer.edac.ReedSolomon_63_35_29_P25; import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessageFactory; @@ -214,8 +214,7 @@ public class SacchTimeslot extends AbstractSignalingTimeslot // input[61] = 0; //Shortened // input[62] = 0; //Shortened - //Reed-Solomon(52,30,23) code protects the IOEMI word. Maximum correctable errors are: 14 (58 - 30 / 2) - ReedSolomon_63_35_29 reedSolomon_63_35_29 = new ReedSolomon_63_35_29(14); + ReedSolomon_63_35_29_P25 reedSolomon_63_35_29 = new ReedSolomon_63_35_29_P25(); boolean irrecoverableErrors; diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java index f11b794f..9663b909 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoderState.java @@ -40,17 +40,13 @@ import io.github.dsheirer.protocol.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; -import java.util.function.Consumer; public class PassportDecoderState extends DecoderState { @@ -408,10 +404,10 @@ public class PassportDecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; - case SOURCE_FREQUENCY: + case NOTIFICATION_SOURCE_FREQUENCY: mFrequency = event.getFrequency(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java index db7574ef..9717ec4d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java @@ -73,15 +73,6 @@ public class Tait1200Decoder extends AbstractAFSKDecoder implements IBinarySymbo return DecoderType.TAIT_1200; } - public void dispose() - { - super.dispose(); - mMessageFramerGPS.dispose(); - mMessageFramerANI.dispose(); - mMessageAProcessor.dispose(); - mMessageBProcessor.dispose(); - } - @Override public void process(boolean symbol) { diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java index a5310683..f0380ba0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200DecoderState.java @@ -155,7 +155,7 @@ public class Tait1200DecoderState extends DecoderState { switch(event.getEvent()) { - case RESET: + case REQUEST_RESET: resetState(); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/traffic/TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/traffic/TrafficChannelManager.java new file mode 100644 index 00000000..d53df513 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/traffic/TrafficChannelManager.java @@ -0,0 +1,29 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.traffic; + +import io.github.dsheirer.module.Module; + +/** + * Traffic Channel Manager base class + */ +public abstract class TrafficChannelManager extends Module +{ +} diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index 1eda7cb2..560a9c66 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -137,25 +137,6 @@ public class AMDemodulatorModule extends Module implements ISourceEventListener, return this; } - @Override - public void dispose() - { - if(mIQFilter != null) - { - mIQFilter.dispose(); - mIQFilter = null; - } - - if(mLowPassFilter != null) - { - mLowPassFilter.dispose(); - mLowPassFilter = null; - } - - mDemodulator = null; - mAGC = null; - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index 4fa81782..4c779007 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -76,19 +76,6 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener, return mSourceEventProcessor; } - @Override - public void dispose() - { - if(mIQFilter != null) - { - mIQFilter.dispose(); - mIQFilter = null; - } - - mDemodulator.dispose(); - mDemodulator = null; - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/module/log/DecodeEventLogger.java b/src/main/java/io/github/dsheirer/module/log/DecodeEventLogger.java index c1f630c8..566aa9ca 100644 --- a/src/main/java/io/github/dsheirer/module/log/DecodeEventLogger.java +++ b/src/main/java/io/github/dsheirer/module/log/DecodeEventLogger.java @@ -71,12 +71,6 @@ public class DecodeEventLogger extends EventLogger implements IDecodeEventListen return this; } - @Override - public void dispose() - { - super.stop(); - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/module/log/MessageEventLogger.java b/src/main/java/io/github/dsheirer/module/log/MessageEventLogger.java index 9ba3737d..e09616a6 100644 --- a/src/main/java/io/github/dsheirer/module/log/MessageEventLogger.java +++ b/src/main/java/io/github/dsheirer/module/log/MessageEventLogger.java @@ -45,12 +45,6 @@ public class MessageEventLogger extends EventLogger implements IMessageListener, return this; } - @Override - public void dispose() - { - super.stop(); - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java index c268afeb..169a0d76 100644 --- a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java +++ b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java @@ -21,6 +21,7 @@ */ package io.github.dsheirer.playlist; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; @@ -244,7 +245,8 @@ public class PlaylistManager implements Listener JacksonXmlModule xmlModule = new JacksonXmlModule(); xmlModule.setDefaultUseWrapper(false); - ObjectMapper objectMapper = new XmlMapper(xmlModule); + ObjectMapper objectMapper = new XmlMapper(xmlModule) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try(InputStream in = Files.newInputStream(path)) { @@ -483,7 +485,8 @@ public class PlaylistManager implements Listener JacksonXmlModule xmlModule = new JacksonXmlModule(); xmlModule.setDefaultUseWrapper(false); - ObjectMapper objectMapper = new XmlMapper(xmlModule); + ObjectMapper objectMapper = new XmlMapper(xmlModule) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try(InputStream in = Files.newInputStream(files.getPlaylist())) { @@ -505,7 +508,8 @@ public class PlaylistManager implements Listener JacksonXmlModule xmlModule = new JacksonXmlModule(); xmlModule.setDefaultUseWrapper(false); - ObjectMapper objectMapper = new XmlMapper(xmlModule); + ObjectMapper objectMapper = new XmlMapper(xmlModule) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try(InputStream in = Files.newInputStream(files.getLegacyPlaylist())) { diff --git a/src/main/java/io/github/dsheirer/preference/UserPreferences.java b/src/main/java/io/github/dsheirer/preference/UserPreferences.java index 0181d3bf..b301c98e 100644 --- a/src/main/java/io/github/dsheirer/preference/UserPreferences.java +++ b/src/main/java/io/github/dsheirer/preference/UserPreferences.java @@ -208,6 +208,6 @@ public class UserPreferences implements Listener @Override public void receive(PreferenceType preferenceType) { - MyEventBus.getEventBus().post(preferenceType); + MyEventBus.getGlobalEventBus().post(preferenceType); } } diff --git a/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java b/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java index 05a6b777..26bfc457 100644 --- a/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java +++ b/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java @@ -29,6 +29,7 @@ import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.preference.Preference; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter; @@ -40,7 +41,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.EnumMap; -import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.prefs.Preferences; @@ -150,6 +150,7 @@ public class TalkgroupFormatPreference extends Preference case MPT1327: return IntegerFormat.FORMATTED; case APCO25: + case DMR: case MDC1200: case PASSPORT: case UNKNOWN: @@ -168,6 +169,7 @@ public class TalkgroupFormatPreference extends Preference case MPT1327: return IntegerFormat.DECIMAL_FORMATTED; case APCO25: + case DMR: case MDC1200: case PASSPORT: case UNKNOWN: @@ -184,6 +186,7 @@ public class TalkgroupFormatPreference extends Preference case PASSPORT: return false; case APCO25: + case DMR: case FLEETSYNC: case LTR: case LTR_NET: @@ -311,6 +314,9 @@ public class TalkgroupFormatPreference extends Preference case APCO25: return APCO25TalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.APCO25), isTalkgroupFixedWidth(Protocol.APCO25)); + case DMR: + return DMRTalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.DMR), + isTalkgroupFixedWidth(Protocol.DMR)); case FLEETSYNC: return FleetsyncTalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.FLEETSYNC), isTalkgroupFixedWidth(Protocol.FLEETSYNC)); @@ -362,6 +368,9 @@ public class TalkgroupFormatPreference extends Preference case APCO25: return APCO25TalkgroupFormatter.format(radioIdentifier, getTalkgroupFormat(Protocol.APCO25), isTalkgroupFixedWidth(Protocol.APCO25)); + case DMR: + return DMRTalkgroupFormatter.format(radioIdentifier, getTalkgroupFormat(Protocol.DMR), + isTalkgroupFixedWidth(Protocol.DMR)); case PASSPORT: return PassportTalkgroupFormatter.format(radioIdentifier, getTalkgroupFormat(Protocol.PASSPORT), isTalkgroupFixedWidth(Protocol.PASSPORT)); diff --git a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/DMRTalkgroupFormatter.java b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/DMRTalkgroupFormatter.java new file mode 100644 index 00000000..31193481 --- /dev/null +++ b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/DMRTalkgroupFormatter.java @@ -0,0 +1,113 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.preference.identifier.talkgroup; + +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.preference.identifier.IntegerFormat; + +public class DMRTalkgroupFormatter extends AbstractIntegerFormatter +{ + public static final int GROUP_DECIMAL_WIDTH = 8; + public static final int UNIT_DECIMAL_WIDTH = 8; + public static final int GROUP_HEXADECIMAL_WIDTH = 6; + public static final int UNIT_HEXADECIMAL_WIDTH = 6; + + /** + * Formats the individual or group identifier to the specified format and width. + */ + public static String format(TalkgroupIdentifier identifier, IntegerFormat format, boolean fixedWidth) + { + if(fixedWidth) + { + switch(format) + { + case DECIMAL: + case FORMATTED: + return toDecimal(identifier.getValue(), GROUP_DECIMAL_WIDTH); + case HEXADECIMAL: + return toHex(identifier.getValue(), GROUP_HEXADECIMAL_WIDTH); + default: + throw new IllegalArgumentException("Unrecognized integer format: " + format); + } + } + else + { + switch(format) + { + case DECIMAL: + case FORMATTED: + return identifier.getValue().toString(); + case HEXADECIMAL: + return toHex(identifier.getValue()); + default: + throw new IllegalArgumentException("Unrecognized integer format: " + format); + } + } + } + + /** + * Formats the radio identifier to the specified format and width. + */ + public static String format(RadioIdentifier identifier, IntegerFormat format, boolean fixedWidth) + { + if(fixedWidth) + { + switch(format) + { + case DECIMAL: + case FORMATTED: + return toDecimal(identifier.getValue(), UNIT_DECIMAL_WIDTH); + case HEXADECIMAL: + return toHex(identifier.getValue(), UNIT_HEXADECIMAL_WIDTH); + default: + throw new IllegalArgumentException("Unrecognized integer format: " + format); + } + } + else + { + switch(format) + { + case DECIMAL: + case FORMATTED: + return identifier.getValue().toString(); + case HEXADECIMAL: + return toHex(identifier.getValue()); + default: + throw new IllegalArgumentException("Unrecognized integer format: " + format); + } + } + } + + @Override + public String format(int value, IntegerFormat integerFormat) + { + switch(integerFormat) + { + case DECIMAL: + case FORMATTED: + return format(value); + case HEXADECIMAL: + return toHex(value); + default: + throw new IllegalArgumentException("Unrecognized integer format: " + integerFormat); + } + } +} diff --git a/src/main/java/io/github/dsheirer/protocol/Protocol.java b/src/main/java/io/github/dsheirer/protocol/Protocol.java index a30dfc42..9f2f066b 100644 --- a/src/main/java/io/github/dsheirer/protocol/Protocol.java +++ b/src/main/java/io/github/dsheirer/protocol/Protocol.java @@ -30,16 +30,20 @@ public enum Protocol { APCO25("APCO-25", "APCO25PHASE1", 9600), APCO25_PHASE2("APCO-25 P2", "APCO25PHASE2", 12000), + ARS("ARS", "ARS", 0), CELLOCATOR("CELLOCATOR", "CELLOCATOR", 0), + DMR("DMR", "DMR", 9600), FLEETSYNC("Fleetsync", "FLEETSYNC", 1200), IPV4("IPV4", "IPV4", 0), LOJACK("LoJack", "LOJACK", 1200), + LRRP("LRRP", "LRRP", 0), LTR("LTR", "LTR", 300), LTR_NET("LTR-Net", "LTRNET", 300), MDC1200("MDC-1200", "MDC1200", 1200), MPT1327("MPT-1327", "MPT1327", 1200), PASSPORT("Passport", "PASSPORT", 300), TAIT1200("Tait 1200", "TAIT1200", 1200), + UDP("UDP", "UDP", 0), UNKNOWN("Unknown", "UNKNOWN", 0); private String mLabel; @@ -54,8 +58,10 @@ public enum Protocol mBitRate = bitRate; } - public static EnumSet TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, FLEETSYNC, LTR, MDC1200, MPT1327, PASSPORT); - public static EnumSet RADIO_ID_PROTOCOLS = EnumSet.of(APCO25, PASSPORT); + public static EnumSet TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200, + MPT1327, PASSPORT); + + public static EnumSet RADIO_ID_PROTOCOLS = EnumSet.of(APCO25, DMR, PASSPORT); @Override public String toString() diff --git a/src/main/java/io/github/dsheirer/record/binary/BinaryRecorder.java b/src/main/java/io/github/dsheirer/record/binary/BinaryRecorder.java index ede4bddf..b433437f 100644 --- a/src/main/java/io/github/dsheirer/record/binary/BinaryRecorder.java +++ b/src/main/java/io/github/dsheirer/record/binary/BinaryRecorder.java @@ -140,12 +140,6 @@ public class BinaryRecorder extends Module implements IReusableByteBufferListene return mBufferProcessor; } - @Override - public void dispose() - { - stop(); - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java index 2f19d752..1086f48c 100644 --- a/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/ComplexBufferWaveRecorder.java @@ -149,12 +149,6 @@ public class ComplexBufferWaveRecorder extends Module implements IReusableComple return this; } - @Override - public void dispose() - { - stop(); - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java index 68d434d5..142da7f5 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java @@ -65,7 +65,6 @@ public class ReusableComplexBufferAssembler public void dispose() { mReusableComplexBufferQueue.dispose(); - mListener = null; } /** diff --git a/src/main/java/io/github/dsheirer/source/Source.java b/src/main/java/io/github/dsheirer/source/Source.java index 948f17cc..eebd0489 100644 --- a/src/main/java/io/github/dsheirer/source/Source.java +++ b/src/main/java/io/github/dsheirer/source/Source.java @@ -85,11 +85,6 @@ public abstract class Source extends Module implements ISourceEventListener, ISo */ public abstract long getFrequency(); - /** - * Process any cleanup actions to prepare for garbage collection of this source - */ - public abstract void dispose(); - /** * Registers the listener to receive overflow state changes. Use null argument to clear the listener */ diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 8ea0169e..5ed294ae 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -21,10 +21,15 @@ */ package io.github.dsheirer.source; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.EnumSet; public class SourceEvent { + private final static Logger mLog = LoggerFactory.getLogger(SourceEvent.class); + public enum Event { NOTIFICATION_CHANNEL_COUNT_CHANGE, @@ -46,6 +51,7 @@ public class SourceEvent REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, REQUEST_FREQUENCY_CHANGE, REQUEST_FREQUENCY_ROTATION, + REQUEST_FREQUENCY_SELECTION, REQUEST_SOURCE_DISPOSE, REQUEST_START_SAMPLE_STREAM, REQUEST_STOP_SAMPLE_STREAM; @@ -388,6 +394,15 @@ public class SourceEvent return new SourceEvent(Event.REQUEST_FREQUENCY_ROTATION); } + /** + * Creates a request to cycle to a specific frequency. This is normally used for decoders to request the + * next frequency in a list when a multiple-frequency source configuration is defined. + */ + public static SourceEvent frequencySelectionRequest() + { + return new SourceEvent(Event.REQUEST_FREQUENCY_SELECTION); + } + /** * Creates a notification that the request to rotate the source frequency to the next frequency in the list * was successful. diff --git a/src/main/java/io/github/dsheirer/source/SourceManager.java b/src/main/java/io/github/dsheirer/source/SourceManager.java index 15575e8d..a3174ef7 100644 --- a/src/main/java/io/github/dsheirer/source/SourceManager.java +++ b/src/main/java/io/github/dsheirer/source/SourceManager.java @@ -95,7 +95,7 @@ public class SourceManager if(config instanceof SourceConfigTunerMultipleFrequency) { SourceConfigTunerMultipleFrequency sourceConfigTuner = (SourceConfigTunerMultipleFrequency)config; - TunerChannel tunerChannel = sourceConfigTuner.getFirstTunerChannel(channelSpecification.getBandwidth()); + TunerChannel tunerChannel = sourceConfigTuner.getTunerChannel(channelSpecification.getBandwidth()); String preferredTuner = sourceConfigTuner.getPreferredTuner(); Source source = mTunerModel.getSource(tunerChannel, channelSpecification, preferredTuner); diff --git a/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java b/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java index e0f34549..f2cab64c 100644 --- a/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java +++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor; import java.util.ArrayList; import java.util.List; @@ -37,6 +38,8 @@ public class SourceConfigTunerMultipleFrequency extends SourceConfiguration { private List mFrequencies = new ArrayList<>(); private String mPreferredTuner; + private Long mPreferredFrequency; + private int mFrequencyRotationDelay = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM; public SourceConfigTunerMultipleFrequency() { @@ -69,6 +72,37 @@ public class SourceConfigTunerMultipleFrequency extends SourceConfiguration return mFrequencies.size() > 1; } + /** + * Preferred frequency to start first for this configuration + */ + @JsonIgnore + public long getPreferredFrequency() + { + if(mPreferredFrequency != null) + { + return mPreferredFrequency; + } + else if(mFrequencies.size() > 0) + { + return mFrequencies.get(0); + } + + return 0l; + } + + /** + * Sets which is the preferred frequency to use for this config when it's started + * @param frequency to use first + */ + public void setPreferredFrequency(long frequency) + { + //Only set the frequency if it is one of the frequencies in the list + if(mFrequencies.contains(frequency)) + { + mPreferredFrequency = frequency; + } + } + /** * Adds a frequency to the list */ @@ -113,15 +147,43 @@ public class SourceConfigTunerMultipleFrequency extends SourceConfiguration } @JsonIgnore - public TunerChannel getFirstTunerChannel(int bandwidth) + public TunerChannel getTunerChannel(int bandwidth) { - if(getFrequencies().size() > 0) + return new TunerChannel(getPreferredFrequency(), bandwidth); + } + + /** + * Channel rotation delay. This setting is used when multiple channel frequencies are defined in the source + * config and controls how long the decoder will remaining on each frequency until the channel is identified as + * active or the channel is identified as inactive and a change frequency request is issued. + * + * Note: this value is ignored when the source config contains a single frequency. + * + * @return channel rotation delay in milliseconds. + */ + @JacksonXmlProperty(isAttribute = true, localName = "frequency_rotation_delay") + public int getFrequencyRotationDelay() + { + return mFrequencyRotationDelay; + } + + /** + * Sets the channel rotation delay. + * @param frequencyRotationDelay in milliseconds. + */ + public void setFrequencyRotationDelay(int frequencyRotationDelay) + { + if(frequencyRotationDelay < ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM) { - return new TunerChannel(getFrequencies().get(0), bandwidth); + mFrequencyRotationDelay = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM; + } + else if(frequencyRotationDelay > ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MAXIMUM) + { + mFrequencyRotationDelay = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MAXIMUM; } else { - return new TunerChannel(0l, bandwidth); + mFrequencyRotationDelay = frequencyRotationDelay; } } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java index 35682ebe..6232cc39 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java +++ b/src/main/java/io/github/dsheirer/source/mixer/RealMixerSource.java @@ -140,11 +140,4 @@ public class RealMixerSource extends RealSource { mFrequency = frequency; } - - - @Override - public void dispose() - { - mMixerReader.dispose(); - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/MultiFrequencyTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/MultiFrequencyTunerChannelSource.java index eab2d87e..e81ac76f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/MultiFrequencyTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/MultiFrequencyTunerChannelSource.java @@ -20,6 +20,7 @@ package io.github.dsheirer.source.tuner.channel; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.Source; @@ -27,10 +28,12 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.heartbeat.Heartbeat; import io.github.dsheirer.source.tuner.TunerModel; +import io.github.dsheirer.source.tuner.channel.rotation.FrequencyLockChangeRequest; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -47,6 +50,7 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource private TunerModel mTunerModel; private TunerChannelSource mTunerChannelSource; private List mFrequencies; + private List mLockedFrequencies = new ArrayList<>(); private int mFrequencyListPointer = 0; private ChannelSpecification mChannelSpecification; private Listener mReusableComplexBufferListener; @@ -70,29 +74,34 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource } /** - * Cycles this source to use the next frequency in the list + * Cycles this source to use the next frequency in the list. If no other frequencies are available, + * because of frequency locking, ignore the rotate request. */ - private void nextFrequency() + private void rotate() { if(mChangingChannels.compareAndSet(false, true)) { - //Only change if we have more than one frequency specified - if(mFrequencies.size() > 0) - { - if(mTunerChannelSource != null) - { - //Shutdown the existing tuner channel source - mTunerChannelSource.stop(); - mTunerChannelSource.removeListener(mReusableComplexBufferListener); - mTunerChannelSource.removeSourceEventListener(); - mTunerChannelSource.removeHeartbeatListener(mHeartbeatListener); - mTunerChannelSource.dispose(); - mTunerChannelSource = null; - } + long frequency = getNextFrequency(); - //Request the next tuner channel source - getNextSource(); + if(frequency == 0) + { + mChangingChannels.set(false); + return; } + + if(mTunerChannelSource != null) + { + //Shutdown the existing tuner channel source + mTunerChannelSource.stop(); + mTunerChannelSource.removeListener(mReusableComplexBufferListener); + mTunerChannelSource.removeSourceEventListener(); + mTunerChannelSource.removeHeartbeatListener(mHeartbeatListener); + mTunerChannelSource.dispose(); + mTunerChannelSource = null; + } + + //Request the next tuner channel source + getNextSource(getTunerChannel(frequency)); } } @@ -103,11 +112,10 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource * Note: if this channel source is shutdown by the external consumer, the mStarted flag will be set to false and * the persistent attempts will stop. */ - private void getNextSource() + private void getNextSource(TunerChannel nextChannel) { if(mStarted) { - TunerChannel nextChannel = getNextChannel(); Source source = mTunerModel.getSource(nextChannel, mChannelSpecification, mPreferredTuner); if(source instanceof TunerChannelSource) @@ -117,7 +125,9 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource mTunerChannelSource.setListener(mReusableComplexBufferListener); mTunerChannelSource.addHeartbeatListener(mHeartbeatListener); mTunerChannelSource.start(); + mTunerChannel = nextChannel; mChangingChannels.set(false); + getSourceEventListener().receive(SourceEvent.frequencyRotationSuccessNotification(this, nextChannel.getFrequency())); } @@ -126,7 +136,7 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource if(mTunerChannelSource == null) { getSourceEventListener().receive(SourceEvent.frequencyRotationFailureNotification(this, nextChannel.getFrequency())); - ThreadPool.SCHEDULED.schedule(() -> getNextSource(), 1, TimeUnit.SECONDS); + ThreadPool.SCHEDULED.schedule(() -> getNextSource(getTunerChannel(getNextFrequency())), 500, TimeUnit.MILLISECONDS); } } } @@ -151,23 +161,70 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource { mTunerChannelSource.stop(); mTunerChannelSource.removeSourceEventListener(); + mTunerChannelSource = null; } } + /** + * Processes requests to lock or unlock frequencies for this source that are received over the processing chain + * event bus. A locked frequency will not be used in the frequency rotation list until it is unlocked. + * + * @param request to lock or unlock a frequency + */ + @Subscribe + public void process(FrequencyLockChangeRequest request) + { + if(request.isLockRequest() && !mLockedFrequencies.contains(request.getFrequency())) + { + mLockedFrequencies.add(request.getFrequency()); + } + else if(request.isUnlockRequest() && mLockedFrequencies.contains(request.getFrequency())) + { + mLockedFrequencies.remove(request.getFrequency()); + } + } + + /** + * Identifies the next frequency in the list to use, while respecting the locked frequencies list. + * + * @return next frequency or 0 if there currently are no other frequencies than the frequency currently in use. + */ + private long getNextFrequency() + { + if(mFrequencies.size() <= 1) + { + return 0; + } + + int attempts = 0; + + mFrequencyListPointer = ++mFrequencyListPointer % mFrequencies.size(); + + long frequency = mFrequencies.get(mFrequencyListPointer); + + while((mLockedFrequencies.contains(frequency) || frequency == getFrequency()) && attempts < mFrequencies.size()) + { + //If we loop through the entire list back to the current frequency, return 0; + if(frequency == getFrequency()) + { + return 0; + } + + mFrequencyListPointer = ++mFrequencyListPointer % mFrequencies.size(); + + frequency = mFrequencies.get(mFrequencyListPointer); + + attempts++; + } + + return frequency; + } + /** * Provides the next tuner channel from the frequency list. */ - private TunerChannel getNextChannel() + private TunerChannel getTunerChannel(long frequency) { - mFrequencyListPointer++; - - if(mFrequencyListPointer >= mFrequencies.size()) - { - mFrequencyListPointer = 0; - } - - long frequency = mFrequencies.get(mFrequencyListPointer); - return new TunerChannel(frequency, mChannelSpecification.getBandwidth()); } @@ -291,7 +348,7 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource { if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_FREQUENCY_ROTATION) { - nextFrequency(); + rotate(); } else if(mTunerChannelSource != null) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index b6e55d2a..011bedf3 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -40,7 +40,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class); private static final long BUFFER_PROCESSOR_RUN_INTERVAL_MILLISECONDS = 50; private SourceEventListenerToProcessorAdapter mConsumerSourceEventListenerAdapter; - private TunerChannel mTunerChannel; + protected TunerChannel mTunerChannel; private Listener mProducerSourceEventListener; private Listener mConsumerSourceEventListener; private ScheduledIntervalProcessor mScheduledIntervalProcessor = new ScheduledIntervalProcessor(); @@ -117,12 +117,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc return mTunerChannel; } - @Override - public void dispose() - { - stop(); - } - /** * Starts this tuner channel source producing sample stream. */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/AddChannelRotationActiveStateRequest.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/AddChannelRotationActiveStateRequest.java new file mode 100644 index 00000000..4450ca88 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/AddChannelRotationActiveStateRequest.java @@ -0,0 +1,49 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.source.tuner.channel.rotation; + +import io.github.dsheirer.channel.state.State; +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request to add a channel state to the list of active channel states monitored by the channel rotation monitor. + */ +public class AddChannelRotationActiveStateRequest extends ModuleEventBusMessage +{ + private State mState; + + /** + * Constructs an instance + * @param state to add + */ + public AddChannelRotationActiveStateRequest(State state) + { + mState = state; + } + + /** + * State to add to the list of active states + * @return state + */ + public State getState() + { + return mState; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelRotationMonitor.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java similarity index 51% rename from src/main/java/io/github/dsheirer/source/tuner/channel/ChannelRotationMonitor.java rename to src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java index 799ae272..fa4f2547 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelRotationMonitor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java @@ -1,32 +1,28 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * This 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 + * **************************************************************************** */ -package io.github.dsheirer.source.tuner.channel; +package io.github.dsheirer.source.tuner.channel.rotation; import com.google.common.eventbus.Subscribe; import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.channel.state.IDecoderStateEventListener; import io.github.dsheirer.channel.state.State; -import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.module.Module; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.preference.UserPreferences; @@ -43,36 +39,38 @@ import java.util.concurrent.TimeUnit; /** * Monitors channel state to detect when a channel is not in an identified active state and issues a request to rotate - * the next channel frequency in the list. This class depends on the ChannelState to provide a continuous - * stream of channel active/inactive messaging in the form of DecoderStateEvents. + * to the next channel frequency in the list. This class depends on the ChannelState providing a continuous + * stream of channel state notification events in the form of DecoderStateEvents. */ public class ChannelRotationMonitor extends Module implements ISourceEventProvider, IDecoderStateEventListener, Listener { - private final static Logger mLog = LoggerFactory.getLogger(ChannelRotationMonitor.class); + public static final int CHANNEL_ROTATION_DELAY_MINIMUM = 200; + public static final int CHANNEL_ROTATION_DELAY_MAXIMUM = 2000; + private final static Logger mLog = LoggerFactory.getLogger(ChannelRotationMonitor.class); private UserPreferences mUserPreferences; private Collection mActiveStates; private ScheduledFuture mScheduledFuture; private Listener mSourceEventListener; private long mRotationDelay; - private boolean mActive; - private long mInactiveTimestamp = System.currentTimeMillis(); + private long mLastActiveTimestamp = System.currentTimeMillis(); + private boolean mEnabled = true; /** - * Constructs a channel rotation monitor. - * + * Constructs a channel rotation monitor that uses the specified rotation delay. * @param activeStates to monitor - * @param userPreferences to receive updates for rotation delay value from user preferences + * @param rotationDelay specifies how long to remain on each frequency before rotating (in milliseconds). */ - public ChannelRotationMonitor(Collection activeStates, UserPreferences userPreferences) + public ChannelRotationMonitor(Collection activeStates, long rotationDelay) { mActiveStates = activeStates; - mUserPreferences = userPreferences; - mRotationDelay = mUserPreferences.getChannelMultiFrequencyPreference().getRotationDelay(); + mRotationDelay = rotationDelay; - //Register to receive user preference updates - MyEventBus.getEventBus().register(this); + if(mRotationDelay == 0) + { + mRotationDelay = 200; + } } /** @@ -100,21 +98,40 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid } @Override - public void receive(DecoderStateEvent decoderStateEvent) + public void receive(DecoderStateEvent event) { - if(decoderStateEvent.getEvent() == DecoderStateEvent.Event.NOTIFICATION_CHANNEL_ACTIVE_STATE) + switch(event.getEvent()) { - mActive = true; + case NOTIFICATION_CHANNEL_STATE: + if(mActiveStates.contains(event.getState())) + { + mLastActiveTimestamp = System.currentTimeMillis(); + } + break; } - else if(decoderStateEvent.getEvent() == DecoderStateEvent.Event.NOTIFICATION_CHANNEL_INACTIVE_STATE) - { - if(mActive) - { - //If we're toggling from active to inactive, update the timestamp, - mInactiveTimestamp = System.currentTimeMillis(); - } + } - mActive = false; + /** + * Processes a request to disable this monitor instance. + * @param request to disable + */ + @Subscribe + public void disable(DisableChannelRotationMonitorRequest request) + { + mEnabled = false; + stop(); + } + + /** + * Processes a request to add an active state to the list of monitored active states. + * @param request to add + */ + @Subscribe + public void addActiveState(AddChannelRotationActiveStateRequest request) + { + if(!mActiveStates.contains(request.getState())) + { + mActiveStates.add(request.getState()); } } @@ -124,11 +141,11 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid */ private void checkState() { - if(mSourceEventListener != null && !mActive && - ((mInactiveTimestamp + mRotationDelay) < System.currentTimeMillis())) + if(mEnabled && mSourceEventListener != null && + ((mLastActiveTimestamp + mRotationDelay) < System.currentTimeMillis())) { mSourceEventListener.receive(SourceEvent.frequencyRotationRequest()); - mInactiveTimestamp = System.currentTimeMillis(); + mLastActiveTimestamp = System.currentTimeMillis(); } } @@ -148,7 +165,6 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid @Override public void reset() { - } @Override @@ -156,7 +172,19 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid { if(mScheduledFuture == null) { - mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> checkState(), 1, 1, TimeUnit.SECONDS); + Runnable runnable = () -> { + try + { + checkState(); + } + catch(Throwable t) + { + mLog.warn("Error while checking state", t); + } + }; + + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(runnable, mRotationDelay, + mRotationDelay / 2, TimeUnit.MILLISECONDS); } } @@ -166,12 +194,8 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid if(mScheduledFuture != null) { mScheduledFuture.cancel(true); + mScheduledFuture = null; } } - - @Override - public void dispose() - { - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/DisableChannelRotationMonitorRequest.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/DisableChannelRotationMonitorRequest.java new file mode 100644 index 00000000..4ee5c72b --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/DisableChannelRotationMonitorRequest.java @@ -0,0 +1,29 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.source.tuner.channel.rotation; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request to disable the channel rotation monitor. + */ +public class DisableChannelRotationMonitorRequest extends ModuleEventBusMessage +{ +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/FrequencyLockChangeRequest.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/FrequencyLockChangeRequest.java new file mode 100644 index 00000000..d510bd42 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/FrequencyLockChangeRequest.java @@ -0,0 +1,94 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.source.tuner.channel.rotation; + +import io.github.dsheirer.module.ModuleEventBusMessage; + +/** + * Request to lock or unlock a frequency from use. + */ +public class FrequencyLockChangeRequest extends ModuleEventBusMessage +{ + private long mFrequency; + private boolean mLock; + + /** + * Constructs an instance + * @param frequency for the request + * @param lock true if this is a lock or false if this is an unlock + */ + public FrequencyLockChangeRequest(long frequency, boolean lock) + { + mFrequency = frequency; + mLock = lock; + } + + /** + * Frequency for this request + */ + public long getFrequency() + { + return mFrequency; + } + + /** + * Indicates if this request is to lock the frequency + */ + public boolean isLockRequest() + { + return mLock; + } + + /** + * Indicates if this request is to unlock the frequency + */ + public boolean isUnlockRequest() + { + return !mLock; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("FREQUENCY ").append(isLockRequest() ? "LOCK" : "UNLOCK").append(" REQUEST: ").append(getFrequency()); + return sb.toString(); + } + + /** + * Utility method to create a lock request + * @param frequency to lock + * @return request + */ + public static FrequencyLockChangeRequest lock(long frequency) + { + return new FrequencyLockChangeRequest(frequency, true); + } + + /** + * Utility method to create an unlock request + * @param frequency to unlock + * @return request + */ + public static FrequencyLockChangeRequest unlock(long frequency) + { + return new FrequencyLockChangeRequest(frequency, false); + } +} diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index c011a416..f17b7035 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -297,12 +297,6 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil mListener = null; } - @Override - public void dispose() - { - mListener = null; - } - @Override public File getFile() { diff --git a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java index e7f2c134..3c1f0213 100644 --- a/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/RealWaveSource.java @@ -256,12 +256,6 @@ public class RealWaveSource extends RealSource implements IControllableFileSourc mListener = null; } - @Override - public void dispose() - { - mListener = null; - } - @Override public File getFile() {