From a79ab1c607184bc375a60a4e82eea198cff73cba Mon Sep 17 00:00:00 2001 From: sheirerd Date: Thu, 20 Nov 2025 07:21:45 -0500 Subject: [PATCH] #7 NXDN Decoder WIP --- .idea/misc.xml | 45 +- .../alias/id/radio/RadioFormatter.java | 36 +- .../alias/id/talkgroup/TalkgroupFormat.java | 33 +- .../id/talkgroup/TalkgroupFormatter.java | 4 +- .../audio/codec/mbe/MBECallSequence.java | 15 +- .../codec/mbe/MBECallSequenceConverter.java | 3 +- .../codec/mbe/MBECallSequenceReader.java | 30 +- .../audio => audio/codec/mbe}/VoiceFrame.java | 50 +- .../github/dsheirer/bits/BinaryMessage.java | 59 +- .../dsheirer/bits/CorrectedBinaryMessage.java | 42 +- .../io/github/dsheirer/bits/IntField.java | 96 +- .../io/github/dsheirer/bits/LongField.java | 45 +- .../dsheirer/buffer/FloatCircularBuffer.java | 29 +- .../channel/IBandChannelDescriptor.java | 40 + .../dsheirer/channel/IChannelDescriptor.java | 25 +- .../dsheirer/channel/state/DecoderState.java | 2 +- .../channel/state/MultiChannelState.java | 15 +- .../channel/state/SingleChannelState.java | 16 +- .../channel/state/TimeslotDecoderState.java | 8 +- .../event/ChannelStartProcessingRequest.java | 6 +- .../dsheirer/dsp/filter/FilterFactory.java | 47 +- .../ComplexPolyphaseChannelizerM2.java | 2 +- .../channelizer/PolyphaseChannelManager.java | 46 +- .../channelizer/PolyphaseChannelSource.java | 30 +- .../dsp/filter/design/FilterViewer.java | 87 +- .../io/github/dsheirer/dsp/symbol/Dibit.java | 40 +- .../dsheirer/dsp/symbol/DibitDelayLine.java | 16 +- .../symbol/DibitToByteBufferAssembler.java | 41 +- .../java/io/github/dsheirer/edac/CRCUtil.java | 16 +- .../github/dsheirer/filter/FilterEditor.java | 3 +- .../gui/channel/ChannelSpectrumPanel.java | 140 +- .../gui/playlist/alias/AliasItemEditor.java | 12 +- .../alias/identifier/RadioIdEditor.java | 11 +- .../alias/identifier/RadioIdRangeEditor.java | 11 +- .../alias/identifier/TalkgroupEditor.java | 6 +- .../identifier/TalkgroupRangeEditor.java | 14 +- .../ChannelConfigurationEditorFactory.java | 8 +- .../channel/NXDNConfigurationEditor.java | 718 +++++++++++ .../radioreference/ModeDecoderType.java | 4 +- .../radioreference/RadioReferenceDecoder.java | 62 +- .../playlist/radioreference/SiteEditor.java | 41 +- .../tuner/TunerPreferenceEditor.java | 4 +- .../github/dsheirer/gui/viewer/DmrViewer.java | 7 +- .../dsheirer/gui/viewer/MessagePackage.java | 25 +- .../dsheirer/gui/viewer/MessagePackager.java | 17 +- .../gui/viewer/MessageRecordingViewer.java | 14 +- .../dsheirer/gui/viewer/NxdnViewer.java | 762 +++++++++++ .../dsheirer/gui/viewer/P25P1Viewer.java | 7 +- .../dsheirer/gui/viewer/P25P2Viewer.java | 7 +- .../gui/viewer/sync/FMSyncResultsViewer.java | 123 ++ .../gui/viewer/sync/FMSyncVisualizer.java | 220 ++++ .../io/github/dsheirer/identifier/Form.java | 4 +- ...nnelDescriptorConfigurationIdentifier.java | 13 +- .../encryption/EncryptionKeyIdentifier.java | 10 +- .../carrier/CarrierOffsetProcessor.java | 252 ---- .../module/decode/DecoderFactory.java | 82 +- .../dsheirer/module/decode/DecoderType.java | 36 +- .../module/decode/FeedbackDecoder.java | 39 +- .../decode/analog/AnalogDecoderState.java | 6 +- .../decode/config/DecodeConfiguration.java | 14 +- .../module/decode/dmr/DMRDecoder.java | 58 +- .../decode/dmr/DMRSoftSymbolProcessor.java | 5 +- .../decode/dmr/DMRTrafficChannelManager.java | 6 +- .../module/decode/dmr/DecodeConfigDMR.java | 4 +- .../module/decode/dmr/channel/DMRChannel.java | 6 +- .../ChannelEventTracker.java} | 81 +- .../module/decode/event/DecodeEvent.java | 2 +- .../event/DecodeEventDuplicateDetector.java | 11 +- .../module/decode/event/DecodeEventModel.java | 2 +- .../module/decode/event/DecodeEventPanel.java | 65 +- .../module/decode/event/IDecodeEvent.java | 2 +- .../decode/event/PlottableDecodeEvent.java | 2 +- .../decode/ltrnet/LTRNetDecoderState.java | 6 +- .../decode/ltrnet/channel/LtrNetChannel.java | 11 +- .../ltrstandard/channel/LtrChannel.java | 11 +- .../mpt1327/MPT1327ChannelGrantEvent.java | 8 +- .../mpt1327/channel/MPT1327Channel.java | 12 +- .../module/decode/nxdn/DecodeConfigNXDN.java | 218 ++++ .../decode/nxdn/NXDNChannelEventTracker.java | 60 + .../nxdn/NXDNChannelInfoPreloadData.java | 43 + .../module/decode/nxdn/NXDNDecodeEvent.java | 54 + .../module/decode/nxdn/NXDNDecoder.java | 413 ++++++ .../module/decode/nxdn/NXDNDecoderState.java | 738 +++++++++++ .../module/decode/nxdn/NXDNMessage.java | 155 +++ .../module/decode/nxdn/NXDNMessageFramer.java | 251 ++++ .../decode/nxdn/NXDNMessageProcessor.java | 206 +++ .../nxdn/NXDNNetworkConfigurationMonitor.java | 249 ++++ .../decode/nxdn/NXDNSymbolProcessor.java | 1123 +++++++++++++++++ .../nxdn/NXDNTrafficChannelManager.java | 794 ++++++++++++ .../module/decode/nxdn/SyncTracker.java | 79 ++ .../decode/nxdn/audio/NXDNAudioModule.java | 175 +++ .../nxdn/audio/NXDNCallSequenceRecorder.java | 225 ++++ .../decode/nxdn/channel/ChannelFrequency.java | 110 ++ .../nxdn/channel/ChannelInformation.java | 61 + .../decode/nxdn/channel/NXDNChannel.java | 80 ++ .../decode/nxdn/channel/NXDNChannelDFA.java | 74 ++ .../decode/nxdn/channel/NXDNChannelFake.java | 70 + .../nxdn/channel/NXDNChannelLookup.java | 103 ++ .../channel/ObservableChannelFrequency.java | 97 ++ .../decode/nxdn/identifier/NXDNCategory.java | 54 + .../nxdn/identifier/NXDNEncryptionKey.java | 87 ++ .../NXDNFullyQualifiedRadioIdentifier.java | 62 + ...NXDNFullyQualifiedTalkgroupIdentifier.java | 42 + .../identifier/NXDNRadioAccessNumber.java | 54 + .../nxdn/identifier/NXDNRadioIdentifier.java | 170 +++ .../decode/nxdn/identifier/NXDNSite.java | 47 + .../decode/nxdn/identifier/NXDNSystem.java | 50 + .../identifier/NXDNTalkerAliasIdentifier.java | 43 + .../identifier/NXDNTalkgroupIdentifier.java | 126 ++ .../decode/nxdn/layer1/C4FMEqualizer.java | 313 +++++ .../module/decode/nxdn/layer1/Frame.java | 403 ++++++ .../layer1/sync/NXDNHardSyncDetector.java | 59 + .../layer1/sync/NXDNSoftSyncDetector.java | 71 ++ .../sync/NXDNSoftSyncDetectorScalar.java | 46 + .../sync/NXDNSoftSyncDetectorVector128.java | 58 + .../sync/NXDNSoftSyncDetectorVector256.java | 56 + .../sync/NXDNSoftSyncDetectorVector512.java | 48 + .../sync/NXDNSoftSyncDetectorVector64.java | 51 + .../nxdn/layer1/sync/NXDNSyncDetector.java | 173 +++ .../layer1/sync/NXDNSyncDetectorFactory.java | 58 + .../layer1/sync/ReferenceSyncWaveform.java | 28 + .../module/decode/nxdn/layer2/Direction.java | 28 + .../module/decode/nxdn/layer2/Framing.java | 30 + .../decode/nxdn/layer2/Interleaver.java | 94 ++ .../module/decode/nxdn/layer2/LICH.java | 365 ++++++ .../decode/nxdn/layer2/LICHTracker.java | 219 ++++ .../module/decode/nxdn/layer2/RFChannel.java | 46 + .../decode/nxdn/layer2/SACCHAssembler.java | 99 ++ .../decode/nxdn/layer2/SACCHFragment.java | 96 ++ .../module/decode/nxdn/layer2/Scrambler.java | 76 ++ .../module/decode/nxdn/layer2/Structure.java | 100 ++ .../decode/nxdn/layer3/NXDNLayer3Message.java | 80 ++ .../nxdn/layer3/NXDNMessageFactory.java | 422 +++++++ .../decode/nxdn/layer3/NXDNMessageType.java | 707 +++++++++++ .../decode/nxdn/layer3/UnknownMessage.java | 60 + .../broadcast/AdjacentSiteInformation.java | 393 ++++++ .../AdjacentSiteInformationTypeD.java | 193 +++ .../broadcast/ControlChannelInformation.java | 212 ++++ .../DigitalStationIDInformation.java | 131 ++ .../broadcast/FailureStatusInformation.java | 90 ++ .../IChannelInformationReceiver.java | 39 + .../decode/nxdn/layer3/broadcast/Idle.java | 65 + .../nxdn/layer3/broadcast/Neighbor.java | 39 + .../layer3/broadcast/ServiceInformation.java | 113 ++ .../broadcast/ServiceInformationTypeD.java | 130 ++ .../layer3/broadcast/SiteInformation.java | 252 ++++ .../module/decode/nxdn/layer3/call/Audio.java | 189 +++ .../module/decode/nxdn/layer3/call/Call.java | 73 ++ .../layer3/call/CallConnectionResponse.java | 81 ++ .../decode/nxdn/layer3/call/CallControl.java | 132 ++ .../layer3/call/CallWithOptionalLocation.java | 118 ++ .../decode/nxdn/layer3/call/DataCall.java | 60 + .../nxdn/layer3/call/DataCallAcknowledge.java | 100 ++ .../nxdn/layer3/call/DataCallAssignment.java | 151 +++ .../DataCallAssignmentDuplicateControl.java | 44 + .../DataCallAssignmentDuplicateTraffic.java | 140 ++ .../nxdn/layer3/call/DataCallBlock.java | 57 + .../nxdn/layer3/call/DataCallHeader.java | 105 ++ .../layer3/call/DataCallReceptionRequest.java | 84 ++ .../call/DataCallReceptionResponse.java | 94 ++ .../nxdn/layer3/call/DataCallRequest.java | 84 ++ .../nxdn/layer3/call/DataCallResponse.java | 94 ++ .../call/DataCallWithOptionalLocation.java | 60 + .../decode/nxdn/layer3/call/Disconnect.java | 99 ++ .../nxdn/layer3/call/DisconnectRequest.java | 99 ++ .../decode/nxdn/layer3/call/HeaderDelay.java | 83 ++ .../nxdn/layer3/call/IPacketHeader.java | 74 ++ .../layer3/call/InitializationVector.java | 73 ++ .../nxdn/layer3/call/RemoteControl.java | 143 +++ .../layer3/call/RemoteControlRequest.java | 101 ++ .../call/RemoteControlRequestWithESN.java | 102 ++ .../layer3/call/RemoteControlResponse.java | 94 ++ .../call/RemoteControlResponseWithESN.java | 91 ++ .../nxdn/layer3/call/ShortDataCallBlock.java | 62 + .../call/ShortDataCallRequestHeader.java | 94 ++ .../layer3/call/ShortDataCallResponse.java | 116 ++ .../call/ShortDataInitializationVector.java | 52 + .../decode/nxdn/layer3/call/StatusCall.java | 58 + .../layer3/call/StatusInquiryRequest.java | 83 ++ .../layer3/call/StatusInquiryResponse.java | 107 ++ .../nxdn/layer3/call/StatusRequest.java | 101 ++ .../nxdn/layer3/call/StatusResponse.java | 92 ++ .../nxdn/layer3/call/TransmissionRelease.java | 120 ++ .../call/TransmissionReleaseExtension.java | 60 + .../decode/nxdn/layer3/call/UserData.java | 100 ++ .../decode/nxdn/layer3/call/VoiceCall.java | 93 ++ .../nxdn/layer3/call/VoiceCallAssignment.java | 151 +++ .../VoiceCallAssignmentDuplicateControl.java | 44 + .../VoiceCallAssignmentDuplicateTraffic.java | 136 ++ .../call/VoiceCallConnectionRequest.java | 84 ++ .../call/VoiceCallConnectionResponse.java | 104 ++ .../call/VoiceCallInitializationVector.java | 53 + .../call/VoiceCallReceptionRequest.java | 159 +++ .../call/VoiceCallReceptionResponse.java | 104 ++ .../nxdn/layer3/call/VoiceCallRequest.java | 166 +++ .../nxdn/layer3/call/VoiceCallResponse.java | 115 ++ .../call/VoiceCallWithOptionalLocation.java | 69 + .../nxdn/layer3/coding/Convolution.java | 161 +++ .../decode/nxdn/layer3/coding/NXDNCRC.java | 201 +++ .../decode/nxdn/layer3/coding/Path.java | 241 ++++ .../nxdn/layer3/coding/PunctureProvider.java | 142 +++ .../PunctureProviderCACAndFACCH2_UDCH.java | 52 + .../PunctureProviderFACCH1_UDCH2_FACCH3.java | 51 + .../coding/PunctureProviderLongCAC.java | 58 + .../layer3/coding/PunctureProviderNone.java | 60 + .../coding/PunctureProviderSACCHAndSCCH.java | 52 + .../module/decode/nxdn/layer3/data/GPS.java | 238 ++++ .../nxdn/layer3/data/NXDNPacketMessage.java | 50 + .../nxdn/layer3/data/PacketSequence.java | 89 ++ .../layer3/data/PacketSequenceAssembler.java | 92 ++ .../layer3/data/PacketSequenceAssembly.java | 167 +++ .../layer3/filter/NXDNAudioMessageFilter.java | 67 + .../filter/NXDNLayer2MessageFilter.java | 67 + .../filter/NXDNLayer3MessageFilter.java | 83 ++ .../layer3/filter/NXDNMessageFilterSet.java | 73 ++ .../nxdn/layer3/mobility/Authentication.java | 110 ++ .../AuthenticationInquiryRequest.java | 83 ++ ...thenticationInquiryRequestMultiSystem.java | 58 + .../AuthenticationInquiryResponse.java | 81 ++ ...henticationInquiryResponseMultiSystem.java | 58 + .../AuthenticationParameterInformation.java | 72 ++ .../mobility/AuthenticationResponse.java | 72 ++ .../nxdn/layer3/mobility/DataWrite.java | 102 ++ .../layer3/mobility/DataWriteAcknowledge.java | 90 ++ .../nxdn/layer3/mobility/DataWriteBlock.java | 61 + .../nxdn/layer3/mobility/DataWriteHeader.java | 86 ++ .../layer3/mobility/GroupRegistration.java | 132 ++ .../mobility/GroupRegistrationRequest.java | 71 ++ .../mobility/GroupRegistrationResponse.java | 85 ++ .../GroupRegistrationResponseTypeD.java | 98 ++ .../nxdn/layer3/mobility/Registration.java | 117 ++ .../mobility/RegistrationClearRequest.java | 68 + .../mobility/RegistrationClearResponse.java | 82 ++ .../layer3/mobility/RegistrationCommand.java | 64 + .../layer3/mobility/RegistrationRequest.java | 118 ++ .../layer3/mobility/RegistrationResponse.java | 83 ++ .../mobility/RegistrationResponseTypeD.java | 128 ++ .../nxdn/layer3/proprietary/Encoding.java | 58 + .../layer3/proprietary/ProprietaryForm.java | 113 ++ .../nxdn/layer3/proprietary/TalkerAlias.java | 108 ++ .../proprietary/TalkerAliasAssembler.java | 97 ++ .../proprietary/TalkerAliasComplete.java | 72 ++ .../scch/CallCompleteDestinationInfo4.java | 92 ++ .../scch/CallInProgressDestinationInfo2.java | 95 ++ .../scch/CallInProgressDestinationInfo4.java | 93 ++ .../layer3/scch/CallInProgressSourceID.java | 77 ++ .../decode/nxdn/layer3/scch/CallInfo.java | 79 ++ .../decode/nxdn/layer3/scch/Information1.java | 94 ++ .../decode/nxdn/layer3/scch/Information3.java | 62 + .../decode/nxdn/layer3/scch/Information4.java | 88 ++ .../scch/InitializationVectorPart1.java | 75 ++ .../scch/InitializationVectorPart2.java | 68 + .../decode/nxdn/layer3/scch/RepeaterFree.java | 61 + .../nxdn/layer3/scch/RepeaterHaltCWID.java | 60 + .../decode/nxdn/layer3/scch/RepeaterIdle.java | 60 + .../module/decode/nxdn/layer3/scch/SCCH.java | 142 +++ .../decode/nxdn/layer3/scch/SCCHMessage.java | 56 + .../decode/nxdn/layer3/scch/SiteID.java | 79 ++ .../nxdn/layer3/type/AdjacentSiteOption.java | 41 + .../decode/nxdn/layer3/type/AudioCodec.java | 42 + .../layer3/type/AuthenticationOption.java | 63 + .../decode/nxdn/layer3/type/Bandwidth.java | 67 + .../nxdn/layer3/type/CallControlOption.java | 95 ++ .../decode/nxdn/layer3/type/CallOption.java | 56 + .../decode/nxdn/layer3/type/CallTimer.java | 105 ++ .../decode/nxdn/layer3/type/CallType.java | 89 ++ .../decode/nxdn/layer3/type/CauseDISC.java | 96 ++ .../decode/nxdn/layer3/type/CauseDREQ.java | 82 ++ .../decode/nxdn/layer3/type/CauseMM.java | 96 ++ .../decode/nxdn/layer3/type/CauseSS.java | 107 ++ .../decode/nxdn/layer3/type/CauseVD.java | 113 ++ .../layer3/type/ChannelAccessInformation.java | 141 +++ .../nxdn/layer3/type/ChannelNotification.java | 48 + .../nxdn/layer3/type/ChannelStructure.java | 106 ++ .../decode/nxdn/layer3/type/CipherType.java | 66 + .../nxdn/layer3/type/ControlCommand.java | 69 + .../nxdn/layer3/type/ControlParameter.java | 74 ++ .../nxdn/layer3/type/DataCallOption.java | 42 + .../nxdn/layer3/type/DataWriteOption.java | 46 + .../decode/nxdn/layer3/type/Delivery.java | 28 + .../module/decode/nxdn/layer3/type/Digit.java | 59 + .../decode/nxdn/layer3/type/Duplex.java | 50 + .../nxdn/layer3/type/ErrorBlockFlag.java | 62 + .../layer3/type/GroupRegistrationOption.java | 63 + .../nxdn/layer3/type/LocationCategory.java | 87 ++ .../decode/nxdn/layer3/type/LocationID.java | 178 +++ .../nxdn/layer3/type/LocationIDPartial.java | 63 + .../layer3/type/LocationOrSystemIDOption.java | 79 ++ .../decode/nxdn/layer3/type/Option.java | 43 + .../nxdn/layer3/type/PacketInformation.java | 135 ++ .../layer3/type/RadioChannelNotation.java | 46 + .../nxdn/layer3/type/RegistrationOption.java | 64 + .../nxdn/layer3/type/ResponseClassType.java | 64 + .../nxdn/layer3/type/ResponseInformation.java | 56 + .../layer3/type/RestrictionInformation.java | 120 ++ .../decode/nxdn/layer3/type/Service.java | 64 + .../decode/nxdn/layer3/type/ServiceInfo.java | 69 + .../decode/nxdn/layer3/type/SiteType.java | 66 + .../nxdn/layer3/type/StationIDOption.java | 90 ++ .../decode/nxdn/layer3/type/Status.java | 89 ++ .../nxdn/layer3/type/StatusCallOption.java | 53 + .../decode/nxdn/layer3/type/Structure.java | 89 ++ .../nxdn/layer3/type/SubscriberType.java | 77 ++ .../decode/nxdn/layer3/type/SystemID.java | 42 + .../nxdn/layer3/type/TransmissionMode.java | 71 ++ .../nxdn/layer3/type/TypeDCallOption.java | 91 ++ .../decode/nxdn/layer3/type/Vendor.java | 77 ++ .../nxdn/layer3/type/VoiceCallOption.java | 47 + .../decode/p25/P25ChannelEventTracker.java | 40 + .../decode/p25/P25ChannelGrantEvent.java | 2 +- .../decode/p25/P25TrafficChannelManager.java | 88 +- .../p25/identifier/channel/APCO25Channel.java | 8 +- .../p25/identifier/channel/P25Channel.java | 6 +- .../identifier/channel/StandardChannel.java | 6 +- .../p25/phase1/DecodeConfigP25Phase1.java | 4 +- .../decode/p25/phase1/P25P1DecoderC4FM.java | 33 +- .../decode/p25/phase1/P25P1DecoderLSM.java | 34 +- .../p25/phase1/P25P1DemodulatorC4FM.java | 5 +- .../p25/phase1/P25P1DemodulatorLSM.java | 3 +- .../p25/phase1/P25P1MessageProcessor.java | 6 +- .../P25P1NetworkConfigurationMonitor.java | 12 +- .../p25/phase1/message/IAdjacentSite.java | 11 +- .../message/IFrequencyBandReceiver.java | 7 +- .../p25/phase1/message/hdu/HeaderData.java | 4 +- ...otorolaGroupRegroupVoiceChannelUpdate.java | 6 +- .../LCAdjacentSiteStatusBroadcast.java | 12 +- ...LCAdjacentSiteStatusBroadcastExplicit.java | 10 +- .../standard/LCGroupVoiceChannelUpdate.java | 8 +- .../LCGroupVoiceChannelUpdateExplicit.java | 6 +- .../lc/standard/LCNetworkStatusBroadcast.java | 12 +- .../LCNetworkStatusBroadcastExplicit.java | 10 +- .../LCProtectionParameterBroadcast.java | 6 +- .../lc/standard/LCRFSSStatusBroadcast.java | 12 +- .../LCRFSSStatusBroadcastExplicit.java | 10 +- .../LCSecondaryControlChannelBroadcast.java | 16 +- ...ondaryControlChannelBroadcastExplicit.java | 10 +- .../message/ldu/EncryptionSyncParameters.java | 4 +- .../p25/phase1/message/ldu/LDU1Message.java | 4 +- .../osp/AMBTCAdjacentStatusBroadcast.java | 10 +- .../ambtc/osp/AMBTCGroupDataChannelGrant.java | 8 +- .../osp/AMBTCGroupVoiceChannelGrant.java | 36 +- .../osp/AMBTCIndividualDataChannelGrant.java | 8 +- ...AMBTCMotorolaGroupRegroupChannelGrant.java | 8 +- .../osp/AMBTCNetworkStatusBroadcast.java | 40 +- .../AMBTCProtectionParameterBroadcast.java | 4 +- .../ambtc/osp/AMBTCRFSSStatusBroadcast.java | 40 +- ...MBTCTelephoneInterconnectChannelGrant.java | 36 +- ...lephoneInterconnectChannelGrantUpdate.java | 36 +- ...BTCUnitToUnitVoiceServiceChannelGrant.java | 8 +- ...tToUnitVoiceServiceChannelGrantUpdate.java | 8 +- .../p25/phase1/message/tdu/TDULCMessage.java | 6 +- .../motorola/osp/MotorolaBaseStationId.java | 40 +- ...laExplicitTDMADataChannelAnnouncement.java | 6 +- .../osp/MotorolaGroupRegroupChannelGrant.java | 8 +- .../MotorolaGroupRegroupChannelUpdate.java | 8 +- .../standard/osp/AdjacentStatusBroadcast.java | 10 +- .../osp/GroupDataChannelAnnouncement.java | 44 +- .../GroupDataChannelAnnouncementExplicit.java | 40 +- .../standard/osp/GroupDataChannelGrant.java | 36 +- .../standard/osp/GroupVoiceChannelGrant.java | 36 +- .../osp/GroupVoiceChannelGrantUpdate.java | 8 +- .../GroupVoiceChannelGrantUpdateExplicit.java | 36 +- .../osp/IndividualDataChannelGrant.java | 40 +- .../standard/osp/NetworkStatusBroadcast.java | 12 +- .../osp/ProtectionParameterUpdate.java | 3 +- .../standard/osp/RFSSStatusBroadcast.java | 12 +- .../SNDCPDataChannelAnnouncementExplicit.java | 18 +- .../standard/osp/SNDCPDataChannelGrant.java | 36 +- .../osp/SecondaryControlChannelBroadcast.java | 44 +- ...ondaryControlChannelBroadcastExplicit.java | 40 +- ...elephoneInterconnectVoiceChannelGrant.java | 36 +- ...neInterconnectVoiceChannelGrantUpdate.java | 36 +- .../osp/UnitToUnitVoiceChannelGrant.java | 36 +- .../UnitToUnitVoiceChannelGrantUpdate.java | 36 +- .../decode/p25/phase2/P25P2Decoder.java | 39 +- .../decode/p25/phase2/P25P2DecoderHDQPSK.java | 5 +- .../decode/p25/phase2/P25P2DecoderState.java | 5 +- .../p25/phase2/P25P2MessageProcessor.java | 8 +- .../P25P2NetworkConfigurationMonitor.java | 10 +- .../EncryptionSynchronizationSequence.java | 5 +- .../AdjacentStatusBroadcastExplicit.java | 6 +- ...jacentStatusBroadcastExtendedExplicit.java | 6 +- .../AdjacentStatusBroadcastImplicit.java | 6 +- .../GroupVoiceChannelGrantExplicit.java | 6 +- .../GroupVoiceChannelGrantImplicit.java | 6 +- .../GroupVoiceChannelGrantUpdateExplicit.java | 6 +- .../GroupVoiceChannelGrantUpdateImplicit.java | 8 +- ...iceChannelGrantUpdateMultipleExplicit.java | 8 +- ...iceChannelGrantUpdateMultipleImplicit.java | 8 +- .../NetworkStatusBroadcastExplicit.java | 6 +- .../NetworkStatusBroadcastImplicit.java | 6 +- .../message/mac/structure/PushToTalk.java | 5 +- .../RfssStatusBroadcastExplicit.java | 6 +- .../RfssStatusBroadcastImplicit.java | 6 +- .../SNDCPDataChannelAnnouncement.java | 6 +- .../mac/structure/SNDCPDataChannelGrant.java | 6 +- ...ondaryControlChannelBroadcastExplicit.java | 10 +- ...ondaryControlChannelBroadcastImplicit.java | 16 +- ...InterconnectVoiceChannelGrantExplicit.java | 6 +- ...InterconnectVoiceChannelGrantImplicit.java | 6 +- ...nitVoiceChannelGrantUpdateAbbreviated.java | 6 +- ...itVoiceChannelGrantUpdateExtendedLCCH.java | 6 +- ...nitVoiceChannelGrantUpdateExtendedVCH.java | 6 +- ...itVoiceServiceChannelGrantAbbreviated.java | 6 +- ...tVoiceServiceChannelGrantExtendedLCCH.java | 6 +- ...itVoiceServiceChannelGrantExtendedVCH.java | 6 +- .../L3HarrisPrivateDataChannelGrant.java | 6 +- .../L3HarrisUnitToUnitDataChannelGrant.java | 6 +- ...orolaGroupRegroupChannelGrantExplicit.java | 6 +- ...orolaGroupRegroupChannelGrantImplicit.java | 6 +- ...otorolaGroupRegroupChannelGrantUpdate.java | 10 +- ...otorolaGroupRegroupVoiceChannelUpdate.java | 8 +- .../motorola/MotorolaTDMADataChannel.java | 8 +- .../module/log/DecodeEventLogger.java | 2 +- .../identifier/TalkgroupFormatPreference.java | 28 +- .../talkgroup/NXDNTalkgroupFormatter.java | 151 +++ .../io/github/dsheirer/protocol/Protocol.java | 5 +- .../dsheirer/record/RecorderFactory.java | 13 +- .../record/binary/BinaryRecorder.java | 7 +- .../radioreference/RadioReference.java | 46 +- ...FrequencyCorrectionStatusNotification.java | 108 ++ .../github/dsheirer/source/SourceEvent.java | 35 +- .../github/dsheirer/source/tuner/Tuner.java | 20 +- .../source/tuner/TunerController.java | 72 +- .../tuner/TunerFrequencyErrorMonitor.java | 135 -- .../channel/HalfBandTunerChannelSource.java | 44 +- .../MultiFrequencyTunerChannelSource.java | 14 +- .../channel/PassThroughChannelSource.java | 27 +- .../tuner/channel/TunerChannelSource.java | 36 +- .../ChannelFrequencyErrorManager.java | 194 +++ .../tuner/frequency/FrequencyController.java | 4 +- .../frequency/TunerFrequencyErrorManager.java | 283 +++++ .../tuner/manager/ChannelSourceManager.java | 9 +- .../FrequencyErrorCorrectionManager.java | 173 --- .../HeterodyneChannelSourceManager.java | 64 +- .../manager/PassThroughSourceManager.java | 12 +- .../PolyphaseChannelSourceManager.java | 21 +- .../dsheirer/source/tuner/ui/TunerEditor.java | 6 +- .../spectrum/FrequencyOverlayPanel.java | 7 +- .../vector/calibrate/CalibrationManager.java | 2 + .../vector/calibrate/CalibrationType.java | 3 +- .../filter/FirFilterCalibration.java | 5 +- .../sync/NXDNSoftSyncCalibration.java | 278 ++++ .../audio/DuplicateCallDetectionTest.java | 7 +- 444 files changed, 31047 insertions(+), 2075 deletions(-) rename src/main/java/io/github/dsheirer/{module/decode/p25/audio => audio/codec/mbe}/VoiceFrame.java (77%) create mode 100644 src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java create mode 100644 src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java create mode 100644 src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java create mode 100644 src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java create mode 100644 src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java delete mode 100644 src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java rename src/main/java/io/github/dsheirer/module/decode/{p25/P25TrafficChannelEventTracker.java => event/ChannelEventTracker.java} (84%) create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNCallSequenceRecorder.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/ChannelFrequency.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/ChannelInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/NXDNChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/NXDNChannelDFA.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/NXDNChannelFake.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/NXDNChannelLookup.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/channel/ObservableChannelFrequency.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNCategory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNEncryptionKey.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNFullyQualifiedRadioIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNFullyQualifiedTalkgroupIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNRadioAccessNumber.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNRadioIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNSite.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNSystem.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNTalkerAliasIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/identifier/NXDNTalkgroupIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/C4FMEqualizer.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/Frame.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNHardSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorScalar.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector128.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector256.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector512.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector64.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetectorFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/ReferenceSyncWaveform.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Direction.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Framing.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Interleaver.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/LICH.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/LICHTracker.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/RFChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/SACCHAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/SACCHFragment.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Scrambler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Structure.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/NXDNLayer3Message.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/NXDNMessageFactory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/NXDNMessageType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/UnknownMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/AdjacentSiteInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/AdjacentSiteInformationTypeD.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/ControlChannelInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/DigitalStationIDInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/FailureStatusInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/IChannelInformationReceiver.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/Idle.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/Neighbor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/ServiceInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/ServiceInformationTypeD.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/broadcast/SiteInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/Audio.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/Call.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/CallConnectionResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/CallControl.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/CallWithOptionalLocation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCall.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallAcknowledge.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallAssignment.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallAssignmentDuplicateControl.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallAssignmentDuplicateTraffic.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallReceptionRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallReceptionResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DataCallWithOptionalLocation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/Disconnect.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/DisconnectRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/HeaderDelay.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/IPacketHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/InitializationVector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControl.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControlRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControlRequestWithESN.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControlResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControlResponseWithESN.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/ShortDataCallBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/ShortDataCallRequestHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/ShortDataCallResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/ShortDataInitializationVector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/StatusCall.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/StatusInquiryRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/StatusInquiryResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/StatusRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/StatusResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/TransmissionRelease.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/TransmissionReleaseExtension.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/UserData.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCall.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallAssignment.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallAssignmentDuplicateControl.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallAssignmentDuplicateTraffic.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallConnectionRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallConnectionResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallInitializationVector.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallReceptionRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallReceptionResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/VoiceCallWithOptionalLocation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Convolution.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/NXDNCRC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Path.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProvider.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderCACAndFACCH2_UDCH.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderFACCH1_UDCH2_FACCH3.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderLongCAC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderNone.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderSACCHAndSCCH.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/data/GPS.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/data/NXDNPacketMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/data/PacketSequence.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/data/PacketSequenceAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/data/PacketSequenceAssembly.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/filter/NXDNAudioMessageFilter.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/filter/NXDNLayer2MessageFilter.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/filter/NXDNLayer3MessageFilter.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/filter/NXDNMessageFilterSet.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/Authentication.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationInquiryRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationInquiryRequestMultiSystem.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationInquiryResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationInquiryResponseMultiSystem.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationParameterInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/AuthenticationResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/DataWrite.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/DataWriteAcknowledge.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/DataWriteBlock.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/DataWriteHeader.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/GroupRegistration.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/GroupRegistrationRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/GroupRegistrationResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/GroupRegistrationResponseTypeD.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/Registration.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationClearRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationClearResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationCommand.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationRequest.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationResponse.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/mobility/RegistrationResponseTypeD.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/proprietary/Encoding.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/proprietary/ProprietaryForm.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/proprietary/TalkerAlias.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/proprietary/TalkerAliasAssembler.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/proprietary/TalkerAliasComplete.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/CallCompleteDestinationInfo4.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/CallInProgressDestinationInfo2.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/CallInProgressDestinationInfo4.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/CallInProgressSourceID.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/CallInfo.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/Information1.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/Information3.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/Information4.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/InitializationVectorPart1.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/InitializationVectorPart2.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/RepeaterFree.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/RepeaterHaltCWID.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/RepeaterIdle.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/SCCH.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/SCCHMessage.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/scch/SiteID.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/AdjacentSiteOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/AudioCodec.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/AuthenticationOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Bandwidth.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CallControlOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CallOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CallTimer.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CallType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CauseDISC.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CauseDREQ.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CauseMM.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CauseSS.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CauseVD.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ChannelAccessInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ChannelNotification.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ChannelStructure.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/CipherType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ControlCommand.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ControlParameter.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/DataCallOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/DataWriteOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Delivery.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Digit.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Duplex.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ErrorBlockFlag.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/GroupRegistrationOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/LocationCategory.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/LocationID.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/LocationIDPartial.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/LocationOrSystemIDOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Option.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/PacketInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/RadioChannelNotation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/RegistrationOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ResponseClassType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ResponseInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/RestrictionInformation.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Service.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/ServiceInfo.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/SiteType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/StationIDOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Status.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/StatusCallOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Structure.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/SubscriberType.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/SystemID.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TransmissionMode.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TypeDCallOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/Vendor.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/VoiceCallOption.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25ChannelEventTracker.java create mode 100644 src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NXDNTalkgroupFormatter.java create mode 100644 src/main/java/io/github/dsheirer/source/ChannelFrequencyCorrectionStatusNotification.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/ChannelFrequencyErrorManager.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/frequency/TunerFrequencyErrorManager.java delete mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/FrequencyErrorCorrectionManager.java create mode 100644 src/main/java/io/github/dsheirer/vector/calibrate/sync/NXDNSoftSyncCalibration.java diff --git a/.idea/misc.xml b/.idea/misc.xml index ee9aaf73..669e0e68 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,50 @@ - + + + + + \ No newline at end of file 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 56cb74c6..a899a61b 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 @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2026 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.alias.id.radio; @@ -26,14 +23,14 @@ 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.NXDNTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter; import io.github.dsheirer.protocol.Protocol; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.text.ParseException; import java.util.EnumMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Protocol-specific formatting of radio id values and parsing of formatted radio id values. @@ -47,6 +44,7 @@ public class RadioFormatter { mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter()); mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter()); + mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter()); 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 8fb332f9..db7a7ed3 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 @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.alias.id.talkgroup; @@ -50,6 +47,8 @@ public enum TalkgroupFormat "MPT-1327 valid ranges are 0-127(prefix)
and 1-8192(ident) (ie. 000-0001 to 127-8192)"), NBFM("*****", 1, 0xFFFF, "1 to 65,535", "NBFM valid value range is 1-65,535"), + NXDN("*****", 1, 0xFFFF, "1 to 65,535", + "NXDN talkgroup valid range is 1 to 65,535"), PASSPORT("*****", 1, 0xFFFF, "1 to 65,535", "Passport valid value range is 1-65,535"), UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215", @@ -139,6 +138,8 @@ public enum TalkgroupFormat return MPT1327; case NBFM: return NBFM; + case NXDN: + return NXDN; case PASSPORT: return PASSPORT; default: 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 f1cdd164..b42ca926 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupForm import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter; import io.github.dsheirer.protocol.Protocol; @@ -59,6 +60,7 @@ public class TalkgroupFormatter mFormatterMap.put(Protocol.LTR_NET, ltr); mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter()); mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter()); + mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter()); mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter()); mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter()); } diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java index e02c07e0..ddb8ff19 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.annotation.JsonRootName; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.encryption.EncryptionKey; -import io.github.dsheirer.module.decode.p25.audio.VoiceFrame; import java.util.ArrayList; import java.util.List; @@ -288,10 +287,18 @@ public class MBECallSequence } else { - mVoiceFrames.add(new VoiceFrame(timestamp, frame)); + add(new VoiceFrame(timestamp, frame)); } } + /** + * Adds the voice frame to the sequence + */ + public void add(VoiceFrame voiceFrame) + { + mVoiceFrames.add(voiceFrame); + } + /** * Adds an encrypted audio frame to this call sequence * @@ -317,6 +324,6 @@ public class MBECallSequence */ public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator) { - mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator)); + add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator)); } } diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java index 497c7b65..60099f3a 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,7 +25,6 @@ import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule; import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder; -import io.github.dsheirer.module.decode.p25.audio.VoiceFrame; import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier; import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; import io.github.dsheirer.preference.UserPreferences; diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java index be9b4864..2cb6b4b5 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2026 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.audio.codec.mbe; @@ -25,7 +22,6 @@ package io.github.dsheirer.audio.codec.mbe; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Joiner; -import io.github.dsheirer.module.decode.p25.audio.VoiceFrame; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java similarity index 77% rename from src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java rename to src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java index c7c9d090..957a73f1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java @@ -1,20 +1,23 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2019 Dennis Sheirer +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer * - * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any - * later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License along with this program. - * If not, see - * - ******************************************************************************/ + * 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.p25.audio; +package io.github.dsheirer.audio.codec.mbe; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; @@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName; */ @JsonRootName(value = "frame") @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({"encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"}) +@JsonPropertyOrder({"tag", "encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"}) public class VoiceFrame { private long mTimestamp; @@ -35,6 +38,7 @@ public class VoiceFrame private Integer mAlgorithm; private Integer mKeyId; private String mMessageIndicator; + private String mTag; public VoiceFrame() { @@ -70,6 +74,24 @@ public class VoiceFrame mMessageIndicator = messageIndicator; } + /** + * Tag value for the voice frame + */ + @JsonProperty("tag") + public String getTag() + { + return mTag; + } + + /** + * Sets the optional tag value for the frame + * @param tag to set + */ + public void setTag(String tag) + { + mTag = tag; + } + /** * Timestamp the frame was transmitted * diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java index 0539f7bd..f181e524 100644 --- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java +++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -306,6 +306,15 @@ public class BinaryMessage extends BitSet } public String toString() + { + return toHexString(); + } + + /** + * Binary representation of the message + * @return message in binary 0/1 format + */ + public String toBinaryString() { StringBuilder sb = new StringBuilder(); @@ -317,6 +326,10 @@ public class BinaryMessage extends BitSet return sb.toString(); } + /** + * Hex string representation of the message + * @return message in hex + */ public String toHexString() { StringBuilder sb = new StringBuilder(); @@ -997,6 +1010,18 @@ public class BinaryMessage extends BitSet return String.format("%0" + width + "X", getInt(field)); } + /** + * Returns the long field formatted as a hex value using zero prefixes to pad the hex character count to fully + * represent the size (width) of the field. + * @param field to parse as hex + * @return hex value. + */ + public String getHex(LongField field) + { + int width = Math.ceilDiv(field.width(), 4); + return String.format("%0" + width + "X", getLong(field)); + } + /** * Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less * than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in @@ -1315,37 +1340,15 @@ public class BinaryMessage extends BitSet throw new IllegalArgumentException("Message must contain only 0-9 and A-F hexadecimal characters"); } - int length = hex.length() / 2; + BinaryMessage message = new BinaryMessage(hex.length() * 4); - if(length * 2 < hex.length()) + for(int x = 0; x < hex.length(); x++) { - length++; + String rawHex = hex.substring(x, x + 1); + message.setInt(Integer.parseInt(rawHex, 16), IntField.length4(x * 4)); } - BinaryMessage buffer = new BinaryMessage(length * 8); - - int bufferOffset = 0; - for(int x = 0; x < hex.length(); x += 2) - { - int endIndex = x + 2; - if(endIndex > hex.length()) - { - endIndex--; - } - - String rawHex = hex.substring(x, endIndex); - - if(rawHex.length() == 1) - { - rawHex = rawHex + "0"; - } - - int value = Integer.parseInt(rawHex, 16); - buffer.setByte(bufferOffset, (byte)(value & 0xFF)); - bufferOffset += 8; - } - - return buffer; + return message; } /** diff --git a/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java b/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java index 3f11a9c6..84412587 100644 --- a/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java +++ b/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ */ package io.github.dsheirer.bits; +import java.nio.ByteOrder; import java.util.BitSet; public class CorrectedBinaryMessage extends BinaryMessage @@ -54,6 +55,45 @@ public class CorrectedBinaryMessage extends BinaryMessage this.xor(message); } + /** + * Creates a new message loaded from the hexadecimal string + * @param hex representing the bits of the message + * @return new message + */ + public static CorrectedBinaryMessage loadHex(String hex) + { + return new CorrectedBinaryMessage(BinaryMessage.loadHex(hex)); + } + + /** + * Overloads the parent method to return this type. + * @param binary message to load + * @return loaded message + */ + public static CorrectedBinaryMessage load(String binary) + { + return new CorrectedBinaryMessage(BinaryMessage.load(binary)); + } + + /** + * Constructs a binary frame from the byte array with length set to the + * number of bits contained in the byte array + */ + public static CorrectedBinaryMessage from(byte[] data, ByteOrder byteOrder) + { + if(byteOrder == ByteOrder.LITTLE_ENDIAN) + { + return new CorrectedBinaryMessage(from(data)); + } + else + { + CorrectedBinaryMessage frame = new CorrectedBinaryMessage(data.length * 8); + BitSet bitSet = BitSet.valueOf(data); + frame.xor(bitSet); + return frame; + } + } + @Override public int getCorrectedBitCount() { diff --git a/src/main/java/io/github/dsheirer/bits/IntField.java b/src/main/java/io/github/dsheirer/bits/IntField.java index 3f6239a2..372fc3cc 100644 --- a/src/main/java/io/github/dsheirer/bits/IntField.java +++ b/src/main/java/io/github/dsheirer/bits/IntField.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -60,7 +60,27 @@ public record IntField(int start, int end) } /** - * Utility constructor method for a field with one four bits of length. + * Utility constructor method for a field with two bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length2(int start) + { + return new IntField(start, (start + 1)); + } + + /** + * Utility constructor method for a field with three bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length3(int start) + { + return new IntField(start, (start + 2)); + } + + /** + * Utility constructor method for a field with four bits of length. * @param start index (inclusive) * @return constructed bit field. */ @@ -70,7 +90,17 @@ public record IntField(int start, int end) } /** - * Utility constructor method for a field with one four bits of length. + * Utility constructor method for a field with five bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length5(int start) + { + return new IntField(start, (start + 4)); + } + + /** + * Utility constructor method for a field with six bits of length. * @param start index (inclusive) * @return constructed bit field. */ @@ -79,6 +109,16 @@ public record IntField(int start, int end) return new IntField(start, (start + 5)); } + /** + * Utility constructor method for a field with seven bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length7(int start) + { + return new IntField(start, (start + 6)); + } + /** * Utility constructor method for a field with one octet of length. * @param start index (inclusive) @@ -89,6 +129,36 @@ public record IntField(int start, int end) return new IntField(start, (start + 7)); } + /** + * Utility constructor method for a field with nine bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length9(int start) + { + return new IntField(start, (start + 8)); + } + + /** + * Utility constructor method for a field with ten bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length10(int start) + { + return new IntField(start, (start + 9)); + } + + /** + * Utility constructor method for a field with eleven bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length11(int start) + { + return new IntField(start, (start + 10)); + } + /** * Utility constructor method for a field with 12 bits of length. * @param start index (inclusive) @@ -99,6 +169,26 @@ public record IntField(int start, int end) return new IntField(start, (start + 11)); } + /** + * Utility constructor method for a field with 14 bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length14(int start) + { + return new IntField(start, (start + 13)); + } + + /** + * Utility constructor method for a field with 15 bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static IntField length15(int start) + { + return new IntField(start, (start + 14)); + } + /** * Utility constructor method for a field with two octets of length. * @param start index (inclusive) diff --git a/src/main/java/io/github/dsheirer/bits/LongField.java b/src/main/java/io/github/dsheirer/bits/LongField.java index 9bf09102..f2325995 100644 --- a/src/main/java/io/github/dsheirer/bits/LongField.java +++ b/src/main/java/io/github/dsheirer/bits/LongField.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +39,15 @@ public record LongField(int start, int end) } } + /** + * Width of the field in bit positions + * @return width + */ + public int width() + { + return end() - start() + 1; + } + /** * Utility constructor method. * @param start index (inclusive) @@ -101,7 +110,17 @@ public record LongField(int start, int end) } /** - * Utility constructor method for a field with two octets of length. + * Utility constructor method for a field with 23 bits of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static LongField length23(int start) + { + return new LongField(start, (start + 22)); + } + + /** + * Utility constructor method for a field with three octets of length. * @param start index (inclusive) * @return constructed bit field. */ @@ -111,7 +130,7 @@ public record LongField(int start, int end) } /** - * Utility constructor method for a field with three octets of length. + * Utility constructor method for a field with four octets of length. * @param start index (inclusive) * @return constructed bit field. */ @@ -119,4 +138,24 @@ public record LongField(int start, int end) { return new LongField(start, (start + 31)); } + + /** + * Utility constructor method for a field with seven octets of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static LongField length56(int start) + { + return new LongField(start, (start + 55)); + } + + /** + * Utility constructor method for a field with eight octets of length. + * @param start index (inclusive) + * @return constructed bit field. + */ + public static LongField length64(int start) + { + return new LongField(start, (start + 63)); + } } diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index e7df429b..87c0ba26 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -98,6 +98,33 @@ public class FloatCircularBuffer return values; } + /** + * Gets all sample values in reverse order. + */ + public float[] getAllReversed() + { + int valuePointer = 0; + float[] values = new float[mBuffer.length]; + int bufferPointer = mBufferPointer - 1; + + if(bufferPointer < 0) + { + bufferPointer = mBuffer.length - 1; + } + + while(valuePointer < values.length) + { + values[valuePointer++] = mBuffer[bufferPointer--]; + + if(bufferPointer < 0) + { + bufferPointer = mBuffer.length - 1; + } + } + + return values; + } + /** * Places the new value into the buffer, overwriting the oldest value * @param newValue to add to the buffer diff --git a/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java b/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java new file mode 100644 index 00000000..d9dee242 --- /dev/null +++ b/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java @@ -0,0 +1,40 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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; + +import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; + +/** + * Frequency band channel descriptor + */ +public interface IBandChannelDescriptor extends IChannelDescriptor +{ + + /** + * Frequency band identifiers (0 - 15) + */ + int[] getFrequencyBandIdentifiers(); + + /** + * Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies + * @param bandIdentifier + */ + void setFrequencyBand(IFrequencyBand bandIdentifier); + +} diff --git a/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java b/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java index 05ccb6f5..81d11eb0 100644 --- a/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java +++ b/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * * This 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,41 +14,29 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ + package io.github.dsheirer.channel; -import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.protocol.Protocol; -/** - * Channel descriptor - */ public interface IChannelDescriptor { /** * Downlink frequency for the channel + * * @return frequency in hertz */ long getDownlinkFrequency(); /** * Uplink frequency for the channel + * * @return frequency in hertz */ long getUplinkFrequency(); - /** - * Frequency band identifiers (0 - 15) - */ - int[] getFrequencyBandIdentifiers(); - - /** - * Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies - * @param bandIdentifier - */ - void setFrequencyBand(IFrequencyBand bandIdentifier); - /** * Indicates if the timeslot count is greater than 1; */ 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 40fc56ee..633898c6 100644 --- a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by 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 05f4da0e..8a68e274 100644 --- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -523,19 +523,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, timeslot)); } - break; - 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: mTimeslots) - { - if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState())) - { - broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), - getChannel().getChannelType().name())); - return; - } - } break; } } 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 5d57868f..abfda065 100644 --- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -392,15 +392,6 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder mIdentifierUpdateNotificationProxy.receive(new IdentifierUpdateNotification( FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, 0)); break; - case NOTIFICATION_MEASURED_FREQUENCY_ERROR: - //Rebroadcast frequency error measurements to external listener if we're currently - //in an active (ie sync locked) state. - if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState())) - { - broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), - getChannel().getChannelType().name())); - } - break; } } } @@ -421,9 +412,8 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder mSquelchController.setSquelchLock(true); break; case REQUEST_CHANGE_CALL_TIMEOUT: - if(event instanceof ChangeChannelTimeoutEvent) + if(event instanceof ChangeChannelTimeoutEvent timeout) { - ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event; mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds()); } case CONTINUATION: @@ -471,7 +461,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder * Proxy between the internal identifier collection and the external update notification listener. This proxy * enables access to internal components to broadcast silent identifier update notifications externally. */ - public class IdentifierUpdateNotificationProxy implements Listener + public static class IdentifierUpdateNotificationProxy implements Listener { private Listener mIdentifierUpdateNotificationListener; diff --git a/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java index 5d69c524..d60b90de 100644 --- a/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ package io.github.dsheirer.channel.state; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; @@ -89,9 +89,9 @@ public abstract class TimeslotDecoderState extends DecoderState { setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifier).getValue()); } - else if(identifier instanceof IChannelDescriptor) + else if(identifier instanceof IBandChannelDescriptor) { - setCurrentChannel((IChannelDescriptor)identifier); + setCurrentChannel((IBandChannelDescriptor)identifier); } else if(identifier instanceof FrequencyConfigurationIdentifier fci) { 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 index acac9284..620eb5bf 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java +++ b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,13 +19,13 @@ package io.github.dsheirer.controller.channel.event; +import io.github.dsheirer.channel.IBandChannelDescriptor; 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; @@ -66,7 +66,7 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage * @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, + public ChannelStartProcessingRequest(Channel channel, IBandChannelDescriptor channelDescriptor, IdentifierCollection identifierCollection) { this(channel, channelDescriptor, identifierCollection, 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 fc754be6..410b942b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -963,11 +963,11 @@ public class FilterFactory * @return filter coefficients. * @throws FilterDesignException if the requested length is not odd */ - public static float[] getSinc(double cutoff, int length, WindowType windowType) throws FilterDesignException + public static float[] getSinc(double cutoff, int length, WindowType windowType) { if(length % 2 == 0) { - throw new FilterDesignException("Sinc filters must be odd-length"); + throw new IllegalArgumentException("Sinc filters must be odd-length"); } float[] coefficients = new float[length]; @@ -983,7 +983,46 @@ public class FilterFactory for(int x = 1; x <= half; x++) { double a = piScalor * x; - double coefficient = scalor * FastMath.sin(a) / a; + double coefficient = scalor * (FastMath.sin(a) / a); + + coefficient *= window[half + x]; + coefficients[half + x] = (float)coefficient; + coefficients[half - x] = (float)coefficient; + } + + return coefficients; + } + + /** + * Creates a windowed-sync (Nyquist) filter. + * + * @param cutoff frequency (0.0 <> 0.5) + * @param length of the filter - must be odd-length + * @param windowType to use in designing the filter + * @return filter coefficients. + * @throws FilterDesignException if the requested length is not odd + */ + public static float[] getInverseSync(double cutoff, int length, WindowType windowType) + { + if(length % 2 == 0) + { + throw new IllegalArgumentException("Sinc filters must be odd-length"); + } + + float[] coefficients = new float[length]; + int half = length / 2; + + float[] window = WindowFactory.getWindow(windowType, length); + + double scalor = 2.0 * cutoff; + double piScalor = FastMath.PI * scalor; + + coefficients[half] = (float)(1.0 * scalor * window[half]); + + for(int x = 1; x <= half; x++) + { + double a = piScalor * x; + double coefficient = scalor * (a / FastMath.sin(a)); coefficient *= window[half + x]; coefficients[half + x] = (float)coefficient; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index df51ecf4..3daa92ad 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 00ba4aee..44cf02b9 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ package io.github.dsheirer.dsp.filter.channelizer; import io.github.dsheirer.buffer.INativeBuffer; -import io.github.dsheirer.buffer.INativeBufferProvider; import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.eventbus.MyEventBus; @@ -73,7 +72,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9; private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); - private INativeBufferProvider mNativeBufferProvider; + private TunerController mTunerController; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; private SynthesisFilterManager mFilterManager = new SynthesisFilterManager(); @@ -85,23 +84,20 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private boolean mRunning = true; /** - * Creates a polyphase channel manager instance. + * Creates a polyphase channel manager for the tuner controller * - * @param nativeBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer - * streams - * @param frequency of the baseband complex buffer sample stream (ie center frequency) - * @param sampleRate of the baseband complex buffer sample stream + * @param tunerController for a tuner that provides a baseband complex buffer stream. */ - public PolyphaseChannelManager(INativeBufferProvider nativeBufferProvider, long frequency, double sampleRate) + public PolyphaseChannelManager(TunerController tunerController) { - if(nativeBufferProvider == null) + if(tunerController == null) { throw new IllegalArgumentException("Complex buffer provider argument cannot be null"); } - mNativeBufferProvider = nativeBufferProvider; + mTunerController = tunerController; - int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH); + int channelCount = (int)(tunerController.getSampleRate() / MINIMUM_CHANNEL_BANDWIDTH); //Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer if(channelCount % 2 != 0) @@ -109,21 +105,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor channelCount--; } - mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); + mChannelCalculator = new ChannelCalculator(tunerController.getSampleRate(), channelCount, + tunerController.getFrequency(), CHANNEL_OVERSAMPLING); mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10); mBufferDispatcher.setListener(mNativeBufferReceiver); } - /** - * Creates a polyphase channel manager for the tuner controller - * - * @param tunerController for a tuner that provides a baseband complex buffer stream. - */ - public PolyphaseChannelManager(TunerController tunerController) - { - this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate()); - } - /** * Provides a description of the state of this manager. */ @@ -201,7 +188,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor try { channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager, - mChannelSourceEventListener, threadName); + mChannelSourceEventListener, threadName, mTunerController.getTunerFrequencyErrorManager()); mChannelSources.add(channelSource); } @@ -234,7 +221,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //If this is the first channel, register to start the sample buffers flowing if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { - mNativeBufferProvider.addBufferListener(mBufferDispatcher); + mTunerController.addBufferListener(mBufferDispatcher); mPolyphaseChannelizer.start(); mBufferDispatcher.start(); } @@ -263,7 +250,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //If this is the last/only channel, deregister to stop the sample buffers if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { - mNativeBufferProvider.removeBufferListener(mBufferDispatcher); + mTunerController.removeBufferListener(mBufferDispatcher); mBufferDispatcher.stop(); mPolyphaseChannelizer.stop(); } @@ -303,6 +290,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: case NOTIFICATION_RECORDING_FILE_LOADED: + case NOTIFICATION_MEASURED_FREQUENCY_ERROR: //no-op break; case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: @@ -456,9 +444,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source")); } break; - case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED: - //Rebroadcast so that the tuner source can process this event - mSourceEventBroadcaster.broadcast(sourceEvent); + case REQUEST_FREQUENCY_CORRECTION: + case NOTIFICATION_MEASURED_FREQUENCY_ERROR: + //Ignore break; default: mLog.error("Received unrecognized source event from polyphase channel source [" + diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index 95adc4dd..9503277a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,7 @@ import io.github.dsheirer.sample.complex.ComplexSamples; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; @@ -47,6 +48,8 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen private List mOutputProcessorIndexes = new ArrayList<>(); private double mTunerSampleRate; private double mTunerCenterFrequency; + private long mFrequencyCorrection; + private boolean mPendingFrequencyCorrectionUpdate = false; private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate; /** @@ -57,17 +60,27 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen * @param filterManager for access to new or cached synthesis filters * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream) * @param threadName for the channel's dispatcher + * @param tunerFrequencyErrorManager to connect with the channel frequency error manager * @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification */ - public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager, - Listener producerSourceEventListener, String threadName) + public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, + SynthesisFilterManager filterManager, + Listener producerSourceEventListener, String threadName, + TunerFrequencyErrorManager tunerFrequencyErrorManager) throws IllegalArgumentException { - super(producerSourceEventListener, tunerChannel, threadName); + super(producerSourceEventListener, tunerChannel, threadName, tunerFrequencyErrorManager); mChannelSampleRate = channelCalculator.getChannelSampleRate(); doUpdateOutputProcessor(channelCalculator, filterManager); } + @Override + public void setFrequencyCorrection(long correction) + { + mFrequencyCorrection = correction; + mPendingFrequencyCorrectionUpdate = true; + } + /** * Description of the state or configuration of the output processor. */ @@ -280,6 +293,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen doUpdateOutputProcessor(channelCalculator, filterManager); } + //Apply channel decider requested frequency correction + if(mPendingFrequencyCorrectionUpdate) + { + mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset()); + mPendingFrequencyCorrectionUpdate = false; + } + try { if(mPolyphaseChannelOutputProcessor != null) @@ -349,7 +369,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen */ public long getFrequencyOffset() { - return mIndexCenterFrequency - getTunerChannel().getFrequency(); + return mIndexCenterFrequency - getTunerChannel().getFrequency() + mFrequencyCorrection; } @Override 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 698a03ab..9a8aabea 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,9 +19,6 @@ 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.window.WindowType; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; @@ -59,32 +56,68 @@ public class FilterViewer extends Application */ private float[] getFilter() { - int filterLength = 31; - WindowType windowType = WindowType.BLACKMAN_HARRIS_7; +// FIRFilterSpecification specification = FIRFilterSpecification +// .lowPassBuilder() +// .sampleRate(12500) +// .passBandCutoff(2600) +// .passBandAmplitude(1.0).passBandRipple(0.01) +// .stopBandAmplitude(0.0).stopBandStart(3125) +// .stopBandRipple(0.01).build(); +// +// float[] taps = null; +// +// try +// { +// taps = FilterFactory.getTaps(specification); +// } +// catch(Exception fde) //FilterDesignException +// { +// System.out.println("Error"); +// } - FIRFilterSpecification specification = FIRFilterSpecification - .lowPassBuilder() - .sampleRate(12500) - .passBandCutoff(5200) - .passBandAmplitude(1.0).passBandRipple(0.01) //.01 - .stopBandAmplitude(0.0).stopBandStart(6500) //6500 - .stopBandRipple(0.01).build(); + float decimatedSampleRate = 12500f; + int symbolLength = 26; + float rrcAlpha = 0.2f; - float[] taps = null; +// float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / 4800, symbolLength, rrcAlpha); +// float[] taps = FilterFactory.getSinc(1100d / 12500d, 31, WindowType.BLACKMAN_HARRIS_7); +// float[] taps = FilterFactory.getInverseSync(1100d/12500d, 31, WindowType.BLACKMAN_HARRIS_7); - try - { - taps = FilterFactory.getTaps(specification); - } - catch(Exception fde) //FilterDesignException - { - System.out.println("Error"); - } - - // float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType); -// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f); - -// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP; + float[] taps = { +0.031462429f, +0.031747267f, +0.030401148f, +0.027362877f, + +0.022653298f, +0.016379869f, +0.008737200f, +0.000003302f, + -0.009468531f, -0.019262057f, -0.028914291f, -0.037935027f, + -0.045828927f, -0.052119261f, -0.056372283f, -0.058221106f, + -0.057387924f, -0.053703443f, -0.047122444f, -0.037734535f, + -0.025769308f, -0.011595336f, +0.004287292f, +0.021260954f, + +0.038610717f, +0.055550276f, +0.071252765f, +0.084885375f, + +0.095646450f, +0.102803611f, +0.105731303f, +0.103946126f, + +0.097138329f, +0.085197939f, +0.068234131f, +0.046586711f, + +0.020828821f, -0.008239664f, -0.039608255f, -0.072081234f, + -0.104311776f, -0.134843790f, -0.162160200f, -0.184736015f, + -0.201094346f, -0.209863285f, -0.209831516f, -0.200000470f, + -0.179630919f, -0.148282051f, -0.105841323f, -0.052543664f, + +0.011020985f, +0.083912428f, +0.164857408f, +0.252278939f, + +0.344336996f, +0.438979335f, +0.534000832f, +0.627109358f, + +0.715995947f, +0.798406824f, +0.872214756f, +0.935487176f, + +0.986548646f, +1.024035395f, +1.046939951f, +1.054644241f, + +1.046939951f, +1.024035395f, +0.986548646f, +0.935487176f, + +0.872214756f, +0.798406824f, +0.715995947f, +0.627109358f, + +0.534000832f, +0.438979335f, +0.344336996f, +0.252278939f, + +0.164857408f, +0.083912428f, +0.011020985f, -0.052543664f, + -0.105841323f, -0.148282051f, -0.179630919f, -0.200000470f, + -0.209831516f, -0.209863285f, -0.201094346f, -0.184736015f, + -0.162160200f, -0.134843790f, -0.104311776f, -0.072081234f, + -0.039608255f, -0.008239664f, +0.020828821f, +0.046586711f, + +0.068234131f, +0.085197939f, +0.097138329f, +0.103946126f, + +0.105731303f, +0.102803611f, +0.095646450f, +0.084885375f, + +0.071252765f, +0.055550276f, +0.038610717f, +0.021260954f, + +0.004287292f, -0.011595336f, -0.025769308f, -0.037734535f, + -0.047122444f, -0.053703443f, -0.057387924f, -0.058221106f, + -0.056372283f, -0.052119261f, -0.045828927f, -0.037935027f, + -0.028914291f, -0.019262057f, -0.009468531f, +0.000003302f, + +0.008737200f, +0.016379869f, +0.022653298f, +0.027362877f, + +0.030401148f, +0.031747267f, +0.031462429f + }; if(taps == null) { 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 656a9bb4..dadd0bc4 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -52,6 +52,44 @@ public enum Dibit mPhase = phase; } + private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0); + + /** + * Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol. + * @param sample in radians. + * @return symbol decision. + */ + public static Dibit fromSample(float sample) + { + if(sample > 0) + { + return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1; + } + else + { + return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1; + } + } + + /** + * Calculates the error from the sample by first mapping it to a symbol and the error is the difference from + * the ideal phase of that symbol. + * @param sample to process + * @return error from ideal + */ + public static float getError(float sample) + { + if(sample > 0) + { + return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3.mPhase - sample : Dibit.D00_PLUS_1.mPhase - sample; + } + else + { + return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3.mPhase - sample : Dibit.D10_MINUS_1.mPhase - sample; + } + } + + /** * Binary value of the symbol */ diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java index 9ad6403a..daf1438f 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -73,6 +73,18 @@ public class DibitDelayLine } } + /** + * Inserts the dibit array into the buffer + * @param dibits to fill + */ + public void fill(Dibit[] dibits) + { + for(Dibit dibit: dibits) + { + insert(dibit); + } + } + /** * Adjusts the pointer to correct a dibit stuff/delete. * @param offset to adjust. @@ -110,7 +122,7 @@ public class DibitDelayLine public void log() { StringBuilder sb = new StringBuilder(); - + sb.append("Pointer [").append(mPointer).append("] "); int pointer = mPointer; for(int i = 0; i < mDelayLine.length; i++) { diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java index 159d7a6b..606e243a 100644 --- a/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java +++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This 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,6 +20,7 @@ package io.github.dsheirer.dsp.symbol; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.IByteBufferProvider; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +30,7 @@ import org.slf4j.LoggerFactory; */ public class DibitToByteBufferAssembler implements Listener, IByteBufferProvider { - private final static Logger mLog = LoggerFactory.getLogger(DibitToByteBufferAssembler.class); - + private final static Logger LOGGER = LoggerFactory.getLogger(DibitToByteBufferAssembler.class); private ByteBuffer mCurrentBuffer; private int mBufferSize; private byte mCurrentByte; @@ -53,14 +53,21 @@ public class DibitToByteBufferAssembler implements Listener, IByteBufferP * the buffer pointer to zero so that new dibits can be loaded. */ private void getNextBuffer() + { + closeAndBroadcast(); + mCurrentBuffer = ByteBuffer.allocate(mBufferSize); + } + + /** + * Closes the current buffer and broadcasts it to an optionally registered listener + */ + private void closeAndBroadcast() { if(mCurrentBuffer != null && mBufferListener != null) { mCurrentBuffer.flip(); mBufferListener.receive(mCurrentBuffer); } - - mCurrentBuffer = ByteBuffer.allocate(mBufferSize); } @Override @@ -88,17 +95,33 @@ public class DibitToByteBufferAssembler implements Listener, IByteBufferP if(mDibitCount >= 4) { - mCurrentBuffer.put(mCurrentByte); - mCurrentByte = 0x00; - mDibitCount = 0; - if(!mCurrentBuffer.hasRemaining()) { getNextBuffer(); } + + try + { + mCurrentBuffer.put(mCurrentByte); + } + catch (BufferOverflowException e) + { + //This shouldn't happen. + LOGGER.info("Detected buffer overflow in dibit buffer assembler - continuing"); + } + mCurrentByte = 0x00; + mDibitCount = 0; } } + /** + * Flush any buffered bits to the registered listener. + */ + public void flush() + { + closeAndBroadcast(); + } + /** * Registers the listener to receive fully assembled byte buffers from this assembler. */ diff --git a/src/main/java/io/github/dsheirer/edac/CRCUtil.java b/src/main/java/io/github/dsheirer/edac/CRCUtil.java index 572c1218..e9bafd94 100644 --- a/src/main/java/io/github/dsheirer/edac/CRCUtil.java +++ b/src/main/java/io/github/dsheirer/edac/CRCUtil.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -139,7 +139,7 @@ public class CRCUtil BinaryMessage polySet = new BinaryMessage(crcSize + i + 1); polySet.load(i, crcSize + 1, polynomial); message.xor(polySet); - System.out.println(message.toString()); +// System.out.println(message.toString()); } return message; @@ -183,29 +183,29 @@ public class CRCUtil if(!integerArray) { - element.append("l"); + element.append("L"); } element.append(", "); - if(row.length() + element.length() <= 80) + if(row.length() + element.length() <= 120) { - row.append(element.toString()); + row.append(element); } else { - sb.append(row.toString()); + sb.append(row); sb.append("\n"); row = new StringBuilder(); row.append(" "); - row.append(element.toString()); + row.append(element); } } if(row.length() > 4) { - sb.append(row.toString()); + sb.append(row); sb.append("\n"); } diff --git a/src/main/java/io/github/dsheirer/filter/FilterEditor.java b/src/main/java/io/github/dsheirer/filter/FilterEditor.java index 2d5ffec6..cb169ecc 100644 --- a/src/main/java/io/github/dsheirer/filter/FilterEditor.java +++ b/src/main/java/io/github/dsheirer/filter/FilterEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -53,6 +53,7 @@ public class FilterEditor extends JFrame setLayout(new MigLayout("", "[grow,fill]", "[grow,fill][]")); mEditorPanel = new FilterEditorPanel<>(filterSet); JScrollPane scroller = new JScrollPane(mEditorPanel); + scroller.getVerticalScrollBar().setUnitIncrement(16); scroller.setViewportView(mEditorPanel); add(scroller, "wrap"); JButton close = new JButton("Close"); diff --git a/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java index 0e8d1e4a..65374ef3 100644 --- a/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,6 +33,7 @@ import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule; import io.github.dsheirer.settings.SettingsManager; +import io.github.dsheirer.source.ChannelFrequencyCorrectionStatusNotification; import io.github.dsheirer.source.Source; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource; @@ -62,6 +63,7 @@ import org.slf4j.LoggerFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JLayeredPane; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JSplitPane; @@ -75,6 +77,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener { + String message = "Not Implemented"; + + if(mFeedbackDecoder != null) + { + message = mFeedbackDecoder.getEqualizerSettings(); + } + + JOptionPane.showMessageDialog(this, message); + }); + + mBButton = new JButton("Reset"); + mBButton.setToolTipText("Resets the equalizer to default tap values"); + mBButton.addActionListener(e -> { + if(mFeedbackDecoder != null) + { + mFeedbackDecoder.resetEqualizer(); + } + }); + labelPanel.add(mAButton); + labelPanel.add(mBButton); mNoiseFloorSpinnerModel = new SpinnerNumberModel(18, 8, 36, 1); mNoiseFloorSpinnerModel.addChangeListener(e -> { @@ -120,8 +173,8 @@ public class ChannelSpectrumPanel extends JPanel implements Listener { @@ -229,11 +282,15 @@ public class ChannelSpectrumPanel extends JPanel implements Listener { - mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(carrierOffsetFrequency)); - mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(true); - }); - - mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(carrierOffsetFrequency); - } - private void broadcast(SourceEvent sourceEvent) { if(mProcessingChain != null) @@ -327,8 +368,14 @@ public class ChannelSpectrumPanel extends JPanel implements Listener { + mTunerPPMLabel.setText(LABEL_PREFIX_PPM + PPM_FORMAT.format(status.getTunerPPM())); + mTunerPPMLabel.setEnabled(true); + + if(status.isAutoPPM()) + { + mTunerCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_TUNER, status.getTunerCorrection())); + } + else + { + mTunerCorrectionLabel.setText(LABEL_PREFIX_TUNER + " disabled"); + } + mTunerCorrectionLabel.setEnabled(true); + mChannelCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_CHANNEL, status.getChannelCorrection())); + mChannelCorrectionLabel.setEnabled(true); + mDecoderCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_DECODER, status.getDecoderCorrection())); + mDecoderCorrectionLabel.setEnabled(true); + + //We se the offset cursor line to negative so that it aligns with where the signal is located + //instead of the correction that's needed to bring the signal to zero. + mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(-status.getDecoderCorrection()); + }); + } } +//TODO: is this still needed? mSignalPowerView.receive(sourceEvent); } } 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 70b5a9cf..bc96a1a6 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -744,7 +744,6 @@ public class AliasItemEditor extends Editor dmrMenu.getItems().add(new AddRadioIdRangeItem(Protocol.DMR)); dmrMenu.getItems().add(new AddTonesItem("AMBE Audio Tones")); - Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC); fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC)); fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.FLEETSYNC)); @@ -766,6 +765,13 @@ public class AliasItemEditor extends Editor nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM)); nbfmMenu.getItems().add(new AddDcsItem()); + Menu nxdnMenu = new ProtocolMenu(Protocol.NXDN); + nxdnMenu.getItems().add(new AddTalkgroupItem(Protocol.NXDN)); + nxdnMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NXDN)); + nxdnMenu.getItems().add(new AddRadioIdItem(Protocol.NXDN)); + nxdnMenu.getItems().add(new AddRadioIdRangeItem(Protocol.NXDN)); + nxdnMenu.getItems().add(new AddTonesItem("AMBE Audio Tones")); + Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT); passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT)); passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT)); @@ -779,7 +785,7 @@ public class AliasItemEditor extends Editor lojackMenu.getItems().add(new AddLojackItem()); mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, - nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu); + nbfmMenu, nxdnMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu); } return mAddIdentifierButton; 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 30ac1cd1..4f319d91 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.HPos; @@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** * Editor for talkgroup alias identifiers */ @@ -244,6 +243,10 @@ public class RadioIdEditor extends IdentifierEditor "Format: 0 - 16777215")); mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF")); + mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), + "Format: 0 - 65,535")); + mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), + "Format: 0 - FFFF")); 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/RadioIdRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java index b0d7e4b9..18224491 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.HPos; @@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** * Editor for radio ID range alias identifiers */ @@ -293,6 +292,10 @@ public class RadioIdRangeEditor extends IdentifierEditor new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215")); details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF")); + details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), + new IntegerFormatter(0,0xFFFF),"Format: 0 - 65,535")); + details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), + new IntegerFormatter(0,0xFFFF),"Format: 0 - FFFF")); details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF), new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607")); details.add(new RadioIdDetail(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 efdc4b6f..a8c8f69d 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -263,6 +263,10 @@ public class TalkgroupEditor extends IdentifierEditor "Format: 1 - 65535")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), "Format: 1 - FFFF")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF), + "Format: 1 - 65,535")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), + "Format: 1 - FFFF")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java index 4f86bd7b..a9826866 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -298,6 +298,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor new IntegerFormatter(0,65535), "Format: 0 - 65535")); details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), new HexFormatter(0,65535), "Format: 0 - FFFF")); + details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215), + new IntegerFormatter(1,16777215), "Format: 1 - 16777215")); + details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF), + new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF")); details.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED, new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)")); @@ -314,6 +318,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor new IntegerFormatter(0,65535), "Format 0 - 65535")); details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535), new IntegerFormatter(0,65535), "Format 0 - FFFF")); + details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF), + new IntegerFormatter(1,0xFFFF),"Format: 1 - 65,535")); + details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), + new IntegerFormatter(1,0xFFFF),"Format: 1 - FFFF")); details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535")); details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), @@ -324,10 +332,6 @@ public class TalkgroupRangeEditor extends IdentifierEditor new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF")); details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215), new HexFormatter(0,16777215), "Format: 0 - FFFFFF")); - details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215), - new IntegerFormatter(1,16777215), "Format: 1 - 16777215")); - details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF), - new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF")); return details; } 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 c71a77f4..322db01e 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,14 +55,16 @@ public class ChannelConfigurationEditorFactory return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case DMR: return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); - case NBFM: - return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case LTR_NET: return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case LTR: return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case MPT1327: return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); + case NBFM: + return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); + case NXDN: + return new NXDNConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case PASSPORT: return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor); case P25_PHASE1: diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java new file mode 100644 index 00000000..f735e180 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java @@ -0,0 +1,718 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn.DecodeConfigNXDN; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.channel.ObservableChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +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 io.github.dsheirer.source.tuner.manager.TunerManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +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.ToggleButton; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import org.controlsfx.control.SegmentedButton; +import org.controlsfx.control.ToggleSwitch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NXDN channel configuration editor + */ +public class NXDNConfigurationEditor extends ChannelConfigurationEditor +{ + private final static Logger mLog = LoggerFactory.getLogger(NXDNConfigurationEditor.class); + private TitledPane mDecoderPane; + private TitledPane mEventLogPane; + private TitledPane mRecordPane; + private TitledPane mSourcePane; + private SourceConfigurationEditor mSourceConfigurationEditor; + private EventLogConfigurationEditor mEventLogConfigurationEditor; + private RecordConfigurationEditor mRecordConfigurationEditor; + private Spinner mTrafficChannelPoolSizeSpinner; + private TableView mChannelMapTable; + private IntegerTextField mChannelField; + private FrequencyField mDownlinkFrequencyField; + private Button mAddTimeslotFrequencyButton; + private Button mDeleteButton; + private Spinner mChannelRotationDelaySpinner; + private SegmentedButton mTransmissionModeButton; + private ComboBox mEncodingComboBox; + private ToggleSwitch mIgnoreDataCallsToggle; + private ToggleSwitch mIgnoreEncryptedCallsToggle; + + /** + * Constructs an instance + * @param playlistManager for playlists + * @param tunerManager for tuners + * @param userPreferences for preferences + */ + public NXDNConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager, + UserPreferences userPreferences, IFilterProcessor filterProcessor) + { + super(playlistManager, tunerManager, userPreferences, filterProcessor); + getTitledPanesBox().getChildren().add(getSourcePane()); + getTitledPanesBox().getChildren().add(getDecoderPane()); + getTitledPanesBox().getChildren().add(getEventLogPane()); + getTitledPanesBox().getChildren().add(getRecordPane()); + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.NXDN; + } + + 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: NXDN"); + 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 mode = new Label("Mode:"); + GridPane.setHalignment(mode, HPos.RIGHT); + GridPane.setConstraints(mode, 0, row); + gridPane.getChildren().add(mode); + + GridPane.setConstraints(getTransmissionModeButton(), 1, row); + gridPane.getChildren().add(getTransmissionModeButton()); + + Label encoding = new Label("Talker Alias Encoding:"); + GridPane.setHalignment(encoding, HPos.RIGHT); + GridPane.setConstraints(encoding, 2, row); + gridPane.getChildren().add(encoding); + + GridPane.setConstraints(getEncodingComboBox(), 3, row); + gridPane.getChildren().add(getEncodingComboBox()); + + Label poolSizeLabel = new Label("Max Traffic Channels"); + GridPane.setHalignment(poolSizeLabel, HPos.RIGHT); + GridPane.setConstraints(poolSizeLabel, 4, row); + gridPane.getChildren().add(poolSizeLabel); + + GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 5, row); + gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner()); + + Label ignoreLabel = new Label("Ignore:"); + GridPane.setHalignment(ignoreLabel, HPos.RIGHT); + GridPane.setConstraints(ignoreLabel, 0, ++row); + gridPane.getChildren().add(ignoreLabel); + + GridPane.setConstraints(getIgnoreDataCallsToggle(), 1, row); + gridPane.getChildren().add(getIgnoreDataCallsToggle()); + + GridPane.setConstraints(getIgnoreEncryptedCallsToggle(), 2, row, 2, 1); + gridPane.getChildren().add(getIgnoreEncryptedCallsToggle()); + + Label channelMapTableLabel = new Label("Optional Channel Number (LCN) to Frequency Map for Channel-Mode systems. Ignore for Direct Frequency Access (DFA) systems."); + GridPane.setHalignment(channelMapTableLabel, HPos.LEFT); + GridPane.setConstraints(channelMapTableLabel, 0, ++row, 6, 1); + gridPane.getChildren().add(channelMapTableLabel); + + GridPane.setConstraints(getChannelMapTable(), 0, ++row, 6, 3); + gridPane.getChildren().add(getChannelMapTable()); + + VBox buttonsBox = new VBox(); + buttonsBox.setAlignment(Pos.CENTER); + buttonsBox.setSpacing(10); + buttonsBox.getChildren().addAll(getAddTimeslotFrequencyButton(), getDeleteButton()); + + GridPane.setConstraints(buttonsBox, 6, row, 1, 3); + gridPane.getChildren().addAll(buttonsBox); + + row += 3; + + HBox editorBox = new HBox(); + editorBox.setAlignment(Pos.CENTER_LEFT); + editorBox.setSpacing(5); + + Label lcnLabel = new Label("Channel Number"); + editorBox.getChildren().addAll(lcnLabel, getChannelField()); + + Label downlinkLabel = new Label("Downlink Frequency (MHz)"); + downlinkLabel.setPadding(new Insets(0,0,0,5)); + editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField()); + + 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(mTunerManager, + DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MINIMUM_MS, + DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MAXIMUM_MS, + DecodeConfigDMR.CHANNEL_ROTATION_DELAY_DEFAULT_MS); + + //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; + } + + /** + * Talker alias encoding combo box + * @return encoding combo box. + */ + private ComboBox getEncodingComboBox() + { + if(mEncodingComboBox == null) + { + mEncodingComboBox = new ComboBox<>(FXCollections.observableArrayList(Arrays.stream(Encoding.values()).toList())); + mEncodingComboBox.getSelectionModel().select(0); + mEncodingComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + mEncodingComboBox.setTooltip(new Tooltip("Talker Alias Encoding: UTF-8:Latin/English, BIG5:Taiwan")); + } + + return mEncodingComboBox; + } + + private TableView getChannelMapTable() + { + if(mChannelMapTable == null) + { + mChannelMapTable = new TableView<>(FXCollections.observableArrayList(ObservableChannelFrequency.extractor())); + mChannelMapTable.setPrefHeight(100.0); + + TableColumn channelColumn = new TableColumn<>("Channel"); + channelColumn.setPrefWidth(100); + channelColumn.setCellValueFactory(cellData -> cellData.getValue().channelProperty()); + mChannelMapTable.getColumns().add(channelColumn); + mChannelMapTable.getSortOrder().add(channelColumn); + + TableColumn downlinkColumn = new TableColumn<>("Downlink Frequency (MHz)"); + downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().downlinkProperty()); + downlinkColumn.setPrefWidth(200); + mChannelMapTable.getColumns().add(downlinkColumn); + + mChannelMapTable.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> setChannelFrequency(newValue)); + } + + return mChannelMapTable; + } + + private SegmentedButton getTransmissionModeButton() + { + if(mTransmissionModeButton == null) + { + mTransmissionModeButton = new SegmentedButton(); + mTransmissionModeButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK); + mTransmissionModeButton.setDisable(true); + + ToggleButton tb4800 = new ToggleButton(TransmissionMode.M4800.getLabel()); + tb4800.setUserData(TransmissionMode.M4800); + mTransmissionModeButton.getButtons().add(tb4800); + + ToggleButton tb9600 = new ToggleButton(TransmissionMode.M9600.getLabel()); + tb9600.setUserData(TransmissionMode.M9600); + mTransmissionModeButton.getButtons().add(tb9600); + + ToggleButton tbTypeD = new ToggleButton(TransmissionMode.TYPE_D.getLabel()); + tbTypeD.setUserData(TransmissionMode.TYPE_D); + mTransmissionModeButton.getButtons().add(tbTypeD); + + mTransmissionModeButton.getToggleGroup().selectToggle(tb4800); + + mTransmissionModeButton.getToggleGroup().selectedToggleProperty() + .addListener((observable, oldValue, newValue) -> { + //Only trigger change detection when both values are not null and not equal to each other + if(oldValue != null && newValue != null && !oldValue.equals(newValue)) + { + //Only set modified if the toggle changed from one to the other + modifiedProperty().set(true); + } + }); + } + + return mTransmissionModeButton; + } + + + + /** + * Sets the specified timeslot frequency into the editor + */ + private void setChannelFrequency(ObservableChannelFrequency channelFrequency) + { + //Preserve the current modified flag state since setting values in the editor will change it. + boolean modified = modifiedProperty().get(); + + getChannelField().setDisable(channelFrequency == null); + getDownlinkFrequencyField().setDisable(channelFrequency == null); + getDeleteButton().setDisable(channelFrequency == null); + + if(channelFrequency != null) + { + getChannelField().set(channelFrequency.channelProperty().get()); + getDownlinkFrequencyField().set(channelFrequency.downlinkProperty().get()); + } + else + { + getChannelField().set(0); + getDownlinkFrequencyField().set(0); + } + + modifiedProperty().set(modified); + } + + private Button getAddTimeslotFrequencyButton() + { + if(mAddTimeslotFrequencyButton == null) + { + mAddTimeslotFrequencyButton = new Button("Add"); + mAddTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE); + mAddTimeslotFrequencyButton.setOnAction(event -> addChannelFrequency()); + } + + return mAddTimeslotFrequencyButton; + } + + /** + * Adds a new channel frequency entry + */ + private void addChannelFrequency() + { + ObservableChannelFrequency ocf = new ObservableChannelFrequency(); + getChannelMapTable().getItems().add(ocf); + getChannelMapTable().scrollTo(ocf); + getChannelMapTable().getSelectionModel().select(ocf); + modifiedProperty().set(true); + } + + private Button getDeleteButton() + { + if(mDeleteButton == null) + { + mDeleteButton = new Button("Delete"); + mDeleteButton.setDisable(true); + mDeleteButton.setMaxWidth(Double.MAX_VALUE); + mDeleteButton.setOnAction(event -> { + ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + getChannelMapTable().getItems().remove(selected); + modifiedProperty().set(true); + } + }); + } + + return mDeleteButton; + } + + private IntegerTextField getChannelField() + { + if(mChannelField == null) + { + mChannelField = new IntegerTextField(); + mChannelField.setDisable(true); + mChannelField.setPrefWidth(65); + mChannelField.textProperty().addListener((observable, oldValue, newValue) -> { + ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + Integer value = mChannelField.get(); + + if(value != null) + { + selected.channelProperty().set(value); + } + } + + modifiedProperty().set(true); + }); + } + + return mChannelField; + } + + 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) + { + ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem(); + + if(selected != null) + { + selected.downlinkProperty().set(mDownlinkFrequencyField.get()); + } + + modifiedProperty().set(true); + } + }); + } + + return mDownlinkFrequencyField; + } + + 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; + } + + /** + * Toggle switch for ignore data calls. + */ + private ToggleSwitch getIgnoreDataCallsToggle() + { + if(mIgnoreDataCallsToggle == null) + { + mIgnoreDataCallsToggle = new ToggleSwitch("Data Calls"); + mIgnoreDataCallsToggle.setDisable(true); + mIgnoreDataCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true)); + } + + return mIgnoreDataCallsToggle; + } + + /** + * Toggle switch for ignore encrypted calls. + */ + private ToggleSwitch getIgnoreEncryptedCallsToggle() + { + if(mIgnoreEncryptedCallsToggle == null) + { + mIgnoreEncryptedCallsToggle = new ToggleSwitch("Encrypted Calls"); + mIgnoreEncryptedCallsToggle.setDisable(true); + mIgnoreEncryptedCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true)); + } + + return mIgnoreEncryptedCallsToggle; + } + + /** + * 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) + { + getTransmissionModeButton().setDisable(config == null); + getTrafficChannelPoolSizeSpinner().setDisable(config == null); + getChannelMapTable().getItems().clear(); + getChannelMapTable().setDisable(config == null); + getAddTimeslotFrequencyButton().setDisable(config == null); + getDeleteButton().setDisable(true); + getChannelField().set(0); + getChannelField().setDisable(true); + getDownlinkFrequencyField().set(0); + getDownlinkFrequencyField().setDisable(true); + getChannelRotationDelaySpinner().setDisable(config == null); + getIgnoreDataCallsToggle().setDisable(config == null); + getIgnoreEncryptedCallsToggle().setDisable(config == null); + + if(config instanceof DecodeConfigNXDN configNXDN) + { + for(ToggleButton toggle: getTransmissionModeButton().getButtons()) + { + if(toggle.getUserData() == configNXDN.getTransmissionMode()) + { + toggle.setSelected(true); + } + } + + getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(configNXDN.getTrafficChannelPoolSize()); + + for(ChannelFrequency channelFrequency: configNXDN.getChannelMap()) + { + getChannelMapTable().getItems().add(new ObservableChannelFrequency(channelFrequency)); + } + + getIgnoreDataCallsToggle().setSelected(configNXDN.isIgnoreDataCalls()); + getIgnoreEncryptedCallsToggle().setSelected(configNXDN.isIgnoreEncryptedCalls()); + } + else + { + getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0); + getChannelRotationDelaySpinner().getValueFactory().setValue(200); + getIgnoreDataCallsToggle().setSelected(false); + getIgnoreEncryptedCallsToggle().setSelected(false); + } + } + + @Override + protected void saveDecoderConfiguration() + { + DecodeConfigNXDN config = null; + + if(getItem().getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN) + { + config = configNXDN; + } + else + { + config = new DecodeConfigNXDN(TransmissionMode.M4800); + } + + TransmissionMode selected = (TransmissionMode)getTransmissionModeButton().getToggleGroup().selectedToggleProperty().get().getUserData(); + config.setTransmissionMode(selected); + + config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue()); + + List channelFrequencies = new ArrayList<>(); + + for(ObservableChannelFrequency ocf: getChannelMapTable().getItems()) + { + channelFrequencies.add(ocf.getChannel()); + } + + config.setChannelMap(channelFrequencies); + config.setIgnoreDataCalls(getIgnoreDataCallsToggle().isSelected()); + config.setIgnoreEncryptedCalls(getIgnoreEncryptedCallsToggle().isSelected()); + 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); + } +} 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 4d987322..ef7ad35d 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,7 +35,7 @@ public enum ModeDecoderType DSTAR("d-star", null), FM("fm", DecoderType.NBFM), FMN("fmn", DecoderType.NBFM), - NXDN("nxdn", null), + NXDN("nxdn", DecoderType.NXDN), TELM("telm", null), USB("usb", null), LSB("lsb", null), 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 8ef9655b..535f15a7 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,7 +25,10 @@ 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.dmr.identifier.DMRTalkgroup; import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkgroupIdentifier; import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup; import io.github.dsheirer.preference.UserPreferences; @@ -146,10 +149,14 @@ public class RadioReferenceDecoder { case APCO25: return APCO25Talkgroup.create(value); + case DMR: + return DMRTalkgroup.create(value); case LTR: return LTRTalkgroup.create(value); case MPT1327: return MPT1327Talkgroup.createTo(value); + case NXDN: + return NXDNTalkgroupIdentifier.createTo(value); case PASSPORT: return PassportTalkgroup.create(value); default: @@ -385,6 +392,55 @@ public class RadioReferenceDecoder return Collections.emptyList(); } + /** + * Indicates if the specified site is an NXDN site that has site frequencies that can be converted + * to channel frequency map + * @param systemInformation to determine the system type + * @param site containing site frequencies + */ + public boolean hasChannelMap(SystemInformation systemInformation, Site site) + { + Type type = getType(systemInformation); + return type != null && type.getName().contains("NXDN") && !site.getSiteFrequencies().isEmpty(); + } + + /** + * Creates a channel frequency map listing from the radio reference site's list of site frequencies for the site + * @param systemInformation to detect the system type (NXDN) + * @param site containing 1 or more site frequencies + * @return list of timeslot frequency mappings or an empty list. + */ + public List getChannelMap(SystemInformation systemInformation, Site site) + { + if(hasChannelMap(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 + } + } + + frequencies.add(new ChannelFrequency(lcn, (long)(siteFrequency.getFrequency() * 1E6), 0)); + } + + return frequencies; + } + + return Collections.emptyList(); + } + /** * Identifies the sdrtrunk protocol used by the system. * @param system to identify @@ -430,6 +486,7 @@ public class RadioReferenceDecoder } break; case "NXDN": + return Protocol.NXDN; case "EDACS": case "TETRA": case "Midland CMS": @@ -440,6 +497,8 @@ public class RadioReferenceDecoder default: } +// mLog.info("Type:" + type + " Flavor:" + flavor + " Voice:" + voice); + return Protocol.UNKNOWN; } @@ -503,6 +562,7 @@ public class RadioReferenceDecoder } break; case "NXDN": + return DecoderType.NXDN; case "EDACS": case "TETRA": 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 30e01809..5ce2641e 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,9 @@ 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.nxdn.DecodeConfigNXDN; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.phase1.Modulation; import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2; @@ -90,7 +93,6 @@ public class SiteEditor extends GridPane private static final String PRIMARY_CONTROL_CHANNEL = "d"; private static final String TOGGLE_BUTTON_CONTROL = "Control"; private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice"; - private static final String PHASE_2_TDMA_MODULATION = "TDMA"; private static final String PHASE_2_FLAVOR = "Phase II"; private UserPreferences mUserPreferences; @@ -250,6 +252,39 @@ public class SiteEditor extends GridPane .getTimeslotFrequencies(systemInformation, site); dmr.setTimeslotMap(timeslotFrequencies); return dmr; + case NXDN: + Flavor flavor = mRadioReferenceDecoder.getFlavor(systemInformation); + + TransmissionMode mode = TransmissionMode.M4800; + + /** + * 9600 bps Flavors + * ---------------------------- + * 29 - NEXEDGE 9600 + * 45 - Conventional Networked + * + * 4800 bps Flavors + * --------------------------- + * 11 - Narrowband Networked + * 42 - NEXEDGE 4800 + * 44 - Icom IDAS Type C + * + * Type-D (also 4800 bps) Flavors + * --------------------------- + * 36 - Icom IDAS Type D + * 47 - Kenwood Type D + */ + mode = switch(flavor.getName()) + { + case "NEXEDGE 9600", "Conventional Networked" -> TransmissionMode.M9600; + case "Icom IDAS Type D", "Kenwood Type D" -> TransmissionMode.TYPE_D; + default -> TransmissionMode.M4800; + }; + + DecodeConfigNXDN nxdn = new DecodeConfigNXDN(mode); + List channelMap = mRadioReferenceDecoder.getChannelMap(systemInformation, site); + nxdn.setChannelMap(channelMap); + return nxdn; case P25_PHASE1: DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType); @@ -399,7 +434,7 @@ public class SiteEditor extends GridPane getCreateChannelConfigurationButton().setVisible(true); getGoToChannelEditorCheckBox().setVisible(true); getSystemTextField().setText(system.getName()); - getSiteTextField().setText(mCurrentSite.getCountyName()); + getSiteTextField().setText(mCurrentSite.getDescription()); getNameTextField().setText("Control"); if(!siteFrequencies.isEmpty()) diff --git a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java index d633c93c..894ec418 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -115,6 +115,7 @@ public class TunerPreferenceEditor extends HBox Label label = new Label("Please restart the application for this change to take effect"); label.setWrapText(true); Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setHeaderText("Restart Required"); alert.getDialogPane().setContent(label); alert.initOwner(((Node)getChannelizerTypeChoiceBox()).getScene().getWindow()); alert.show(); @@ -183,6 +184,7 @@ public class TunerPreferenceEditor extends HBox Label label = new Label("Please restart the application for this change to take effect"); label.setWrapText(true); Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setHeaderText("Restart Required"); alert.getDialogPane().setContent(label); alert.initOwner(((Node)getRspDuoTunerModeChoiceBox()).getScene().getWindow()); alert.show(); diff --git a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java index 076097eb..af22efc8 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -468,6 +468,10 @@ public class DmrViewer extends VBox mSelectFileButton = new Button("Select ..."); mSelectFileButton.onActionProperty().set(event -> { FileChooser fileChooser = new FileChooser(); + FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits"); + FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*"); + fileChooser.getExtensionFilters().addAll(f1, f2); + fileChooser.setSelectedExtensionFilter(f1); fileChooser.setTitle("Select DMR .bits Recording"); String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null); if(lastDirectory != null) @@ -478,7 +482,6 @@ public class DmrViewer extends VBox fileChooser.setInitialDirectory(file); } } - fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits")); File selected = fileChooser.showOpenDialog(getScene().getWindow()); if(selected != null) diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java index ec49879b..ee10ad27 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,14 +38,35 @@ public class MessagePackage private List mDecodeEvents = new ArrayList<>(); private ChannelStartProcessingRequest mChannelStartProcessingRequest; private AudioSegment mAudioSegment; + private int mBitCounter; + private int mElapsed; /** * Constructs an instance * @param message for this instance + * @param bitCounter at the time this instance was created */ - public MessagePackage(IMessage message) + public MessagePackage(IMessage message, int bitCounter, int elapsed) { mMessage = message; + mBitCounter = bitCounter; + mElapsed = elapsed; + } + + /** + * Bit counter value for this package + */ + public int getBitCounter() + { + return mBitCounter; + } + + /** + * Elapsed bit count from last package + */ + public int getElapsed() + { + return mElapsed; } /** diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java index b4c131d1..42bf6c3c 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,6 +34,8 @@ import io.github.dsheirer.module.decode.event.IDecodeEvent; public class MessagePackager { private MessagePackage mMessagePackage; + private int mBitCounter; + private int mLastBitCount; /** * Constructs an instance @@ -42,6 +44,15 @@ public class MessagePackager { } + /** + * Updates the current bit count + * @param bitCount + */ + public void updateBitCount(int bitCount) + { + mBitCounter = bitCount; + } + /** * Adds an audio segment. * @param audioSegment to add @@ -61,7 +72,9 @@ public class MessagePackager */ public void add(IMessage message) { - mMessagePackage = new MessagePackage(message); + int elapsed = mBitCounter - mLastBitCount; + mMessagePackage = new MessagePackage(message, mBitCounter, elapsed); + mLastBitCount = mBitCounter; } /** diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java index c7ff473e..206054eb 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory; /** * Utility application to load and view .bits recording file with the messages fully parsed. * - * Supported Protocols: DMR, APCO25 Phase 1 and Phase 2. + * Supported Protocols: DMR, NXDN, and APCO25 Phase 1 and Phase 2. */ public class MessageRecordingViewer extends VBox { @@ -50,6 +50,7 @@ public class MessageRecordingViewer extends VBox private MenuBar mMenuBar; private TabPane mTabPane; private int mTabCounterDmr = 1; + private int mTabCounterNxdn = 1; private int mTabCounterP25P1 = 1; private int mTabCounterP25P2 = 1; private UserPreferences mUserPreferences = new UserPreferences(); @@ -77,6 +78,12 @@ public class MessageRecordingViewer extends VBox getTabPane().getTabs().add(tab); getTabPane().getSelectionModel().select(tab); }); + MenuItem nxdnMenuItem = new MenuItem("NXDN"); + nxdnMenuItem.onActionProperty().set(event -> { + Tab tab = new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences)); + getTabPane().getTabs().add(tab); + getTabPane().getSelectionModel().select(tab); + }); MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1"); p25p1MenuItem.onActionProperty().set(event -> { Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)); @@ -89,7 +96,7 @@ public class MessageRecordingViewer extends VBox getTabPane().getTabs().add(tab); getTabPane().getSelectionModel().select(tab); }); - createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem); + createNewViewerMenu.getItems().addAll(dmrMenuItem, nxdnMenuItem, p25p1MenuItem, p25p2MenuItem); MenuItem exitMenu = new MenuItem("Exit"); exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close()); @@ -110,6 +117,7 @@ public class MessageRecordingViewer extends VBox mTabPane = new TabPane(); mTabPane.setMaxHeight(Double.MAX_VALUE); mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())); + mTabPane.getTabs().add(new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences))); mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences))); mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer())); } diff --git a/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java new file mode 100644 index 00000000..75403c85 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java @@ -0,0 +1,762 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.viewer; + +import com.google.common.eventbus.EventBus; +import io.github.dsheirer.channel.state.DecoderStateEvent; +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.identifier.IdentifierUpdateNotification; +import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN; +import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState; +import io.github.dsheirer.module.decode.nxdn.NXDNMessageFramer; +import io.github.dsheirer.module.decode.nxdn.NXDNMessageProcessor; +import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager; +import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader; +import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.record.binary.BinaryReader; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.util.ThreadPool; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.prefs.Preferences; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TablePosition; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NXDN Viewer + */ +public class NxdnViewer extends VBox +{ + private static final Logger mLog = LoggerFactory.getLogger(NxdnViewer.class); + private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); + private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.nxdn"; + private static final String LAST_SELECTED_FILE = "last.selected.file.nxdn"; + private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*"; + private Preferences mPreferences = Preferences.userNodeForPackage(NxdnViewer.class); + private Button mSelectFileButton; + private Label mSelectedFileLabel; + private TableView mMessagePackageTableView; + private ObservableList mMessagePackages = FXCollections.observableArrayList(); + private FilteredList mFilteredMessagePackages = new FilteredList<>(mMessagePackages); + private TextField mSearchText; + private TextField mFindText; + private Button mFindButton; + private Button mFindNextButton; + private ProgressIndicator mLoadingIndicator; + private MessagePackageViewer mMessagePackageViewer; + private StringProperty mLoadedFile = new SimpleStringProperty(); + private UserPreferences mUserPreferences; + + public NxdnViewer(UserPreferences userPreferences) + { + mUserPreferences = userPreferences; + setPadding(new Insets(5)); + setSpacing(5); + + HBox fileBox = new HBox(); + fileBox.setMaxWidth(Double.MAX_VALUE); + fileBox.setAlignment(Pos.CENTER_LEFT); + fileBox.setSpacing(5); + HBox.setHgrow(getSelectFileButton(), Priority.NEVER); + HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS); + getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER); + + fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel()); + + HBox filterBox = new HBox(); + filterBox.setMaxWidth(Double.MAX_VALUE); + filterBox.setAlignment(Pos.BASELINE_CENTER); + filterBox.setSpacing(5); + + Label searchLabel = new Label("Message Filter:"); + HBox.setMargin(searchLabel, new Insets(0,0,0,15)); + Label findLabel = new Label("Find:"); + + HBox.setHgrow(getFindText(), Priority.ALWAYS); + HBox.setHgrow(getSearchText(), Priority.ALWAYS); + + filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel, + getSearchText()); + + VBox.setVgrow(fileBox, Priority.NEVER); + VBox.setVgrow(filterBox, Priority.NEVER); + VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS); + VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER); + + getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer()); + + } + + /** + * Processes the recording file and loads the content into the viewer + * + * Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the + * message table back on the JavaFX UI thread. + * + * @param file containing a .bits recording of decoded DMR data. + */ + private void load(File file) + { + if(file != null && file.exists()) + { + mLoadedFile.set(file.toString()); + mMessagePackages.clear(); + getLoadingIndicator().setVisible(true); + getSelectedFileLabel().setText("Loading ..."); + + ThreadPool.CACHED.submit(() -> { + DecodeConfigNXDN config = new DecodeConfigNXDN(); + List messagePackages = new ArrayList<>(); + NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config); + + Broadcaster messageBroadcaster = new Broadcaster<>(); + messageProcessor.setMessageListener(messageBroadcaster); + + Channel empty = new Channel("Empty"); + TransmissionMode transmissionMode = mLoadedFile.get().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600; + config.setTransmissionMode(transmissionMode); + empty.setDecodeConfiguration(new DecodeConfigNXDN()); + NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode); + + MessagePackager messagePackager = new MessagePackager(); + + //Setup a temporary event bus to capture channel start processing requests + EventBus eventBus = new EventBus("debug"); + eventBus.register(messagePackager); + + NXDNTrafficChannelManager trafficChannelManager = new NXDNTrafficChannelManager(empty); + trafficChannelManager.setInterModuleEventBus(eventBus); + + //Register to receive events + trafficChannelManager.addDecodeEventListener(messagePackager::add); + NXDNDecoderState decoderState = new NXDNDecoderState(empty, trafficChannelManager); + + Broadcaster decoderStateEventBroadcaster = new Broadcaster<>(); + decoderState.setDecoderStateListener(decoderStateEventBroadcaster); + + decoderStateEventBroadcaster.addListener(messagePackager::add); + decoderState.addDecodeEventListener(messagePackager::add); + decoderState.start(); +// + long frequency = getFrequencyFromFile(mLoadedFile.get()); + + if(frequency > 0) + { + trafficChannelManager.setCurrentControlFrequency(frequency, empty); + FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency); + decoderState.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id, + IdentifierUpdateNotification.Operation.ADD, 0)); + } + + messageProcessor.setMessageListener(message -> { + //Add the initial message to the packager so that it can be combined with any decoder state events. + if(message.toString().contains("FROM:166")) + { + int a = 0; + } + messagePackager.add(message); + decoderState.receive(message); + + //Collect the packaged message with events + messagePackages.add(messagePackager.getMessageWithEvents()); + }); + + try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) + { + int bitCount = 0; + + while(reader.hasNext()) + { + ByteBuffer buffer = reader.next(); + for(byte value : buffer.array()) + { + for(int x = 0; x <= 3; x++) + { + messageFramer.processWithHardSyncDetection(Dibit.parse(value, x)); + bitCount += 2; + messagePackager.updateBitCount(bitCount); + } + } + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } + +// audioModule.stop(); +// decoderState.stop(); +// singleChannelState.stop(); + + Platform.runLater(() -> { + getLoadingIndicator().setVisible(false); + getSelectedFileLabel().setText(file.getName()); + mMessagePackages.addAll(messagePackages); + getMessagePackageTableView().scrollTo(0); + }); + }); + } + } + + /** + * Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of + * the decoder states. + * @param file name to parse + * @return parsed frequency or zero. + */ + private static long getFrequencyFromFile(String file) + { + if(file == null || file.isEmpty()) + { + return 0; + } + + if(file.matches(FILE_FREQUENCY_REGEX)) + { + Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX); + Matcher m = p.matcher(file); + if(m.find()) + { + try + { + String raw = m.group(1); + return Long.parseLong(raw); + } + catch(Exception e) + { + mLog.error("Couldn't parse frequency from bits file [" + file + "]"); + } + } + } + + return 0; + } + + /** + * Updates the filter(s) applies to the list of messages + */ + private void updateFilters() + { + String filterText = getSearchText().getText(); + + if(filterText != null && !filterText.isEmpty()) + { + Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase()); + mFilteredMessagePackages.setPredicate(textPredicate); + } + else + { + mFilteredMessagePackages.setPredicate(null); + } + } + + /** + * Finds and selects the first row containing the text argument. + * @param text to search for. + */ + private void find(String text) + { + if(text != null && !text.isEmpty()) + { + for(MessagePackage messagePackage: mFilteredMessagePackages) + { + if(messagePackage.toString().toLowerCase().contains(text.toLowerCase())) + { + getMessagePackageTableView().getSelectionModel().select(messagePackage); + getMessagePackageTableView().scrollTo(messagePackage); + return; + } + } + } + } + + /** + * Finds and selects the first row containing the text argument, after the currently selected row. + * @param text to search for. + */ + private void findNext(String text) + { + if(text != null && !text.isEmpty()) + { + MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem(); + + if(selected == null) + { + find(text); + return; + } + + int row = mFilteredMessagePackages.indexOf(selected); + + for(int x = row + 1; x < mFilteredMessagePackages.size(); x++) + { + if(x < mFilteredMessagePackages.size()) + { + MessagePackage messagePackage = mFilteredMessagePackages.get(x); + + if(messagePackage.toString().toLowerCase().contains(text.toLowerCase())) + { + getMessagePackageTableView().getSelectionModel().select(messagePackage); + getMessagePackageTableView().scrollTo(messagePackage); + return; + } + } + } + } + } + + private MessagePackageViewer getMessagePackageViewer() + { + if(mMessagePackageViewer == null) + { + mMessagePackageViewer = new MessagePackageViewer(); + mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE); + + //Register for table selection events to display the selected value. + getMessagePackageTableView().getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue)); + } + + return mMessagePackageViewer; + } + + /** + * List view control with messages + */ + private TableView getMessagePackageTableView() + { + if(mMessagePackageTableView == null) + { + mMessagePackageTableView = new TableView<>(); + mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + mMessagePackageTableView.setPlaceholder(getLoadingIndicator()); + SortedList sortedList = new SortedList<>(mFilteredMessagePackages); + sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty()); + mMessagePackageTableView.setItems(sortedList); + + mMessagePackageTableView.setOnKeyPressed(event -> + { + if(KEY_CODE_COPY.match(event)) + { + final Set rows = new TreeSet<>(); + for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells()) + { + rows.add(tablePosition.getRow()); + } + + final StringBuilder sb = new StringBuilder(); + boolean firstRow = true; + for (final Integer row : rows) + { + if(firstRow) + { + firstRow = false; + } + else + { + sb.append('\n'); + } + + boolean firstCol = true; + + for (final TableColumn column : mMessagePackageTableView.getColumns()) + { + if(firstCol) + { + firstCol = false; + } + else + { + sb.append('\t'); + } + + final Object cellData = column.getCellData(row); + sb.append(cellData == null ? "" : cellData.toString()); + } + } + final ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(sb.toString()); + Clipboard.getSystemClipboard().setContent(clipboardContent); + } + }); + + TableColumn timestampColumn = new TableColumn(); + timestampColumn.setPrefWidth(110); + timestampColumn.setText("Time"); + timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp")); + + TableColumn bitCountColumn = new TableColumn(); + bitCountColumn.setPrefWidth(90); + bitCountColumn.setText("Bit Counter"); + bitCountColumn.setCellValueFactory(new PropertyValueFactory<>("bitCounter")); + + TableColumn elapsedColumn = new TableColumn(); + elapsedColumn.setPrefWidth(70); + elapsedColumn.setText("Elapsed"); + elapsedColumn.setCellValueFactory(new PropertyValueFactory<>("elapsed")); + + TableColumn validColumn = new TableColumn(); + validColumn.setPrefWidth(50); + validColumn.setText("Valid"); + validColumn.setCellValueFactory(new PropertyValueFactory<>("valid")); + + TableColumn timeslotColumn = new TableColumn(); + timeslotColumn.setPrefWidth(35); + timeslotColumn.setText("TS"); + timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot")); + + TableColumn messageColumn = new TableColumn(); + messageColumn.setPrefWidth(900); + messageColumn.setText("Message"); + messageColumn.setCellValueFactory((Callback) param -> { + SimpleStringProperty property = new SimpleStringProperty(); + if(param.getValue() instanceof MessagePackage messagePackage) + { + property.set(messagePackage.toString()); + } + + return property; + }); + + TableColumn decodeEventCountColumn = new TableColumn(); + decodeEventCountColumn.setPrefWidth(50); + decodeEventCountColumn.setText("Events"); + decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount")); + + TableColumn decoderStateEventCountColumn = new TableColumn(); + decoderStateEventCountColumn.setPrefWidth(50); + decoderStateEventCountColumn.setText("States"); + decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount")); + + TableColumn channelStartCountColumn = new TableColumn(); + channelStartCountColumn.setPrefWidth(50); + channelStartCountColumn.setText("Starts"); + channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount")); + + TableColumn audioSegmentCountColumn = new TableColumn(); + audioSegmentCountColumn.setPrefWidth(50); + audioSegmentCountColumn.setText("Audio"); + audioSegmentCountColumn.setCellValueFactory(new PropertyValueFactory<>("audioSegmentCount")); + + mMessagePackageTableView.getColumns().addAll(timestampColumn, bitCountColumn, elapsedColumn, validColumn, + timeslotColumn, messageColumn, decodeEventCountColumn, decoderStateEventCountColumn, + channelStartCountColumn, audioSegmentCountColumn); + } + + return mMessagePackageTableView; + } + + /** + * File selection button + * @return button + */ + private Button getSelectFileButton() + { + if(mSelectFileButton == null) + { + mSelectFileButton = new Button("Select ..."); + mSelectFileButton.onActionProperty().set(event -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select NXDN .bits Recording"); + FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits"); + FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*"); + fileChooser.getExtensionFilters().addAll(f1, f2); + fileChooser.setSelectedExtensionFilter(f1); + String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null); + if(lastDirectory != null) + { + File file = new File(lastDirectory); + if(file.exists() && file.isDirectory()) + { + fileChooser.setInitialDirectory(file); + + String lastFile = mPreferences.get(LAST_SELECTED_FILE, null); + + if(lastFile != null) + { + fileChooser.initialFileNameProperty().set(lastFile); +// fileChooser.setInitialFileName(lastFile); + } + } + } + final File selected = fileChooser.showOpenDialog(getScene().getWindow()); + + if(selected != null) + { + mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent()); + mPreferences.put(LAST_SELECTED_FILE, selected.getName()); + load(selected); + } + }); + } + + return mSelectFileButton; + } + + /** + * Spinny loading icon to show over the message table view + */ + private ProgressIndicator getLoadingIndicator() + { + if(mLoadingIndicator == null) + { + mLoadingIndicator = new ProgressIndicator(); + mLoadingIndicator.setProgress(-1); + mLoadingIndicator.setVisible(false); + } + + return mLoadingIndicator; + } + + /** + * Selected file path label. + */ + private Label getSelectedFileLabel() + { + if(mSelectedFileLabel == null) + { + mSelectedFileLabel = new Label(" "); + } + + return mSelectedFileLabel; + } + + /** + * Search text filter box + * @return text control for entering search text + */ + private TextField getSearchText() + { + if(mSearchText == null) + { + mSearchText = new TextField(); + mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters()); + } + + return mSearchText; + } + + /** + * Text box for find text + */ + private TextField getFindText() + { + if(mFindText == null) + { + mFindText = new TextField(); + mFindText.setOnKeyPressed(event -> { + if(event.getCode().equals(KeyCode.ENTER)) + { + getFindButton().fire(); + } + }); + mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters()); + } + + return mFindText; + } + + /** + * Find button to search for the text in the find text box. + * @return button + */ + private Button getFindButton() + { + if(mFindButton == null) + { + mFindButton = new Button("Find"); + mFindButton.setOnAction(event -> find(getFindText().getText())); + } + + return mFindButton; + } + + /** + * Find next button to search for the text in the find text box. + * @return button + */ + private Button getFindNextButton() + { + if(mFindNextButton == null) + { + mFindNextButton = new Button("Next"); + mFindNextButton.setOnAction(event -> findNext(getFindText().getText())); + } + + return mFindNextButton; + } + + public static class A implements Listener + { + private Path mOutput; + private int mTarget; + + public A(Path output, int target) + { + mOutput = output; + mTarget = target; + } + + @Override + public void receive(IMessage message) + { + if(message instanceof GPS gps) + { + System.out.println(gps); + + IPacketHeader header = gps.getPacketSequence().getHeader(); + + int id = header.getSource().getValue(); + + if(mTarget != 0 && id != mTarget) + { + return; + } + + try + { + Files.writeString(mOutput, gps + "\n", StandardOpenOption.APPEND); + } + catch(IOException e) + { + e.printStackTrace(); + } + } + } + } + + static void main() throws IOException + { + Path directory = Paths.get("/home/denny/SDRTrunk/recordings"); + int target = 0; + Path output = directory.resolve(target == 0 ? "GPS.txt" : "GPS_" + target + ".txt"); + A a = new A(output, target); + + if(Files.exists(output)) + { + Files.delete(output); + } + + try + { + output.toFile().createNewFile(); + + File[] files = directory.toFile().listFiles(); + + if(files == null) + { + return; + } + + List fileList = Arrays.asList(files); + Collections.sort(fileList); + for(File file: fileList) + { + if(file.getName().endsWith(".bits")) + { + if(file != null && file.exists()) + { + DecodeConfigNXDN config = new DecodeConfigNXDN(); + NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config); + + Channel empty = new Channel("Empty"); + TransmissionMode transmissionMode = file.getName().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600; + config.setTransmissionMode(transmissionMode); + empty.setDecodeConfiguration(new DecodeConfigNXDN()); + NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode); + messageProcessor.setMessageListener(a); + + try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) + { + while(reader.hasNext()) + { + ByteBuffer buffer = reader.next(); + for(byte value : buffer.array()) + { + for(int x = 0; x <= 3; x++) + { + messageFramer.processWithHardSyncDetection(Dibit.parse(value, x)); + } + } + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } + } + } + } + } + catch(IOException ioe) + { + ioe.printStackTrace(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java index 061d5aa4..775efe18 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -508,6 +508,10 @@ public class P25P1Viewer extends VBox mSelectFileButton = new Button("Select ..."); mSelectFileButton.onActionProperty().set(event -> { FileChooser fileChooser = new FileChooser(); + FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits"); + FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*"); + fileChooser.getExtensionFilters().addAll(f1, f2); + fileChooser.setSelectedExtensionFilter(f1); fileChooser.setTitle("Select P25 Phase 1 .bits Recording"); String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null); if(lastDirectory != null) @@ -518,7 +522,6 @@ public class P25P1Viewer extends VBox fileChooser.setInitialDirectory(file); } } - fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits")); final File selected = fileChooser.showOpenDialog(getScene().getWindow()); if(selected != null) diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java index 4166ad43..d31f86c3 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -594,6 +594,10 @@ public class P25P2Viewer extends VBox mSelectFileButton = new Button("Select ..."); mSelectFileButton.onActionProperty().set(event -> { FileChooser fileChooser = new FileChooser(); + FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits"); + FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*"); + fileChooser.getExtensionFilters().addAll(f1, f2); + fileChooser.setSelectedExtensionFilter(f1); fileChooser.setTitle("Select P25 Phase 2 .bits Recording"); String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null); if(lastDirectory != null) @@ -604,7 +608,6 @@ public class P25P2Viewer extends VBox fileChooser.setInitialDirectory(file); } } - fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits")); final File selected = fileChooser.showOpenDialog(getScene().getWindow()); if(selected != null) diff --git a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java new file mode 100644 index 00000000..2a518474 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java @@ -0,0 +1,123 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.viewer.sync; + +import io.github.dsheirer.util.SwingUtils; +import java.util.concurrent.CountDownLatch; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; + +/** + * Utility for viewing sync detection results + */ +public class FMSyncResultsViewer implements ISyncResultsListener +{ + private static final Logger LOGGER = LoggerFactory.getLogger(FMSyncResultsViewer.class.getName()); + private ISyncResultsListener mListener; + + public FMSyncResultsViewer() + { + initUI(); + } + + /** + * View sync detection results. + * @param symbols that were detected + * @param sync that was detected + * @param samples for the symbols + * @param syncIntervals symbol timing interval pointers into the I/Q sample arrays + * @param label to display to the user + * @param release latch to release via the UI when done viewing these results + */ + @Override + public void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, float equalizerBalance, + float equalizerGain, String label, CountDownLatch release) + { + if(mListener != null) + { + mListener.receive(symbols, samples, sync, syncIntervals, equalizerBalance, equalizerGain, label, release); + } + } + + @Override + public void symbol(float symbol) + { + if(mListener != null) + { + mListener.symbol(symbol); + } + } + + /** + * Initializes the viewer UI + */ + private void initUI() + { + CountDownLatch latch = new CountDownLatch(1); + + final JFXPanel fxPanel = new JFXPanel(); + Platform.runLater(() -> { + Platform.setImplicitExit(false); + FMSyncVisualizer syncVisualizer = new FMSyncVisualizer(); + mListener = syncVisualizer; + + Scene scene = new Scene(syncVisualizer, 1400, 1000); + +// URL resource = getClass().getResource("/sdrtrunk_style.css"); +// +// if(resource != null) +// { +// scene.getStylesheets().add(resource.toExternalForm()); +// } +// else +// { +// LOGGER.warn("Can't find stylesheet resource for sdrtrunk"); +// } + + fxPanel.setScene(scene); + fxPanel.setVisible(true); + + JFrame frame = new JFrame(); + frame.setTitle("Sync Results Viewer"); + frame.setContentPane(fxPanel); + frame.setSize(1400, 1400); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLocationRelativeTo(null); + SwingUtils.run(() -> { + frame.setVisible(true); + latch.countDown(); + }); + }); + + try + { + latch.await(); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java new file mode 100644 index 00000000..db235e57 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java @@ -0,0 +1,220 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.viewer.sync; + +import java.util.concurrent.CountDownLatch; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.ScatterChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * JavaFX viewer application for visualizing sample buffers and detected sync patterns + */ +public class FMSyncVisualizer extends VBox implements ISyncResultsListener +{ + private static final int SYMBOL_CHART_SERIES_COUNT = 2000; + private static final float SYMBOL = (float) (Math.PI / 4.0); + private final NumberAxis mConstellationI = new NumberAxis(); + private final NumberAxis mConstellationQ = new NumberAxis(); + private final NumberAxis mSampleTiming = new NumberAxis(); + private final NumberAxis mSamplePhase = new NumberAxis(-4.0, 4.0, .1); + private final NumberAxis mSymbolTiming = new NumberAxis(); + private final NumberAxis mSymbolPhase = new NumberAxis(-Math.PI, Math.PI, Math.PI / 2); + private final XYChart.Series mSymbolSeries = new XYChart.Series<>(); + private final XYChart.Series mSymbolSeriesMarker = new XYChart.Series<>(); + private final ScatterChart mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ); + private final XYChart.Series mConstellationSymbols = new XYChart.Series<>(); + private static final int CONSTELLATION_SYMBOL_COUNT = 50; + private int mConstellationSymbolPointer = 0; + + private final LineChart mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase); + private final ScatterChart mSymbolChart = new ScatterChart<>(mSymbolTiming, mSymbolPhase); + + private final NumberAxis mEqualizerHistoryAxis = new NumberAxis(); + private final NumberAxis mEqualizerValueAxis = new NumberAxis(-14.0, 14.0, .25); + private final LineChart mEqualizerChart = new LineChart<>(mEqualizerHistoryAxis, mEqualizerValueAxis); + private int mEqualizerPointer = 0; + private int mEqualizerHistorySize = 40; + private float[] mEqualizerBalanceHistory = new float[mEqualizerHistorySize]; + private float[] mEqualizerGainHistory = new float[mEqualizerHistorySize]; + + private final Button mReleaseButton = new Button("Next Sync"); + private final Label mPatternLabel = new Label(); + private CountDownLatch mRelease; + private int mSymbolPointer = 0; + + /** + * Constructs an instance + */ + public FMSyncVisualizer() + { + setPadding(new Insets(5)); + mConstellationI.setLabel("Constellation - Inphase"); + mConstellationQ.setLabel("Quadrature"); + mSampleTiming.setLabel("Sample Timing"); + mSamplePhase.setLabel("Sample Phase (+/- PI)"); + + mReleaseButton.setOnAction(e -> { + if(mRelease != null) + { + mRelease.countDown(); + mReleaseButton.setDisable(true); + } + }); + + mConstellationChart.setPrefSize(400, 400); + mConstellationChart.setAnimated(false); + XYChart.Series constellationIdeal = new XYChart.Series<>(); + constellationIdeal.setName("Ideal"); + float PI_4 = (float) (Math.PI / 4); + constellationIdeal.getData().add(new XYChart.Data<>(PI_4, PI_4)); + constellationIdeal.getData().add(new XYChart.Data<>(PI_4, -PI_4)); + constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, PI_4)); + constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, -PI_4)); + mConstellationChart.getData().add(constellationIdeal); + mConstellationSymbols.setName("Symbols"); + + for(int x = 0; x < CONSTELLATION_SYMBOL_COUNT; x++) + { + mConstellationSymbols.getData().add(new XYChart.Data<>(0, 0)); + } + + mConstellationChart.getData().add(mConstellationSymbols); + + mSampleChart.setAnimated(false); + mSymbolChart.setAnimated(false); + mSymbolChart.setId("symbol-chart"); + mEqualizerChart.setAnimated(false); + + mEqualizerValueAxis.setLabel("Value"); + mEqualizerHistoryAxis.setLabel("Equalizer"); + + HBox hbox = new HBox(); + hbox.getChildren().add(mConstellationChart); + HBox.setHgrow(mEqualizerChart, Priority.ALWAYS); + hbox.getChildren().add(mEqualizerChart); + // VBox.setVgrow(hbox, Priority.ALWAYS); + + VBox.setVgrow(mSampleChart, Priority.ALWAYS); + VBox.setVgrow(mSymbolChart, Priority.ALWAYS); + + HBox buttons = new HBox(); + buttons.setAlignment(Pos.BASELINE_LEFT); + buttons.setSpacing(10); + buttons.getChildren().addAll(mReleaseButton, mPatternLabel); + VBox.setVgrow(buttons, Priority.NEVER); + + //Load the symbol chart with symbols + mSymbolSeries.setName("Symbols"); + for(int x = 0; x < SYMBOL_CHART_SERIES_COUNT; x++) + { + mSymbolSeries.getData().add(new XYChart.Data<>(x, 0)); + } + mSymbolChart.getData().add(mSymbolSeries); + float pi_4 = (float)Math.PI / 4f; + mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 3f * pi_4)); + mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, pi_4)); + mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 0)); + mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -pi_4)); + mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -3 * pi_4)); + mSymbolChart.getData().add(mSymbolSeriesMarker); + getChildren().addAll(buttons, hbox, mSampleChart, mSymbolChart); + } + + @Override + public void symbol(float symbol) + { + for(XYChart.Data data : mSymbolSeriesMarker.getData()) + { + data.setXValue(mSymbolPointer); + } + mSymbolSeries.getData().get(mSymbolPointer++).setYValue(symbol); + mSymbolPointer %= SYMBOL_CHART_SERIES_COUNT; + + XYChart.Data element = mConstellationSymbols.getData().get(mConstellationSymbolPointer); + element.setXValue(Math.cos(symbol)); + element.setYValue(Math.sin(symbol)); + mConstellationSymbolPointer++; + mConstellationSymbolPointer %= CONSTELLATION_SYMBOL_COUNT; + } + + @Override + public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, float equalizerBalance, float equalizerGain, String label, CountDownLatch latch) + { + mRelease = latch; + mReleaseButton.setDisable(latch == null); + + mEqualizerBalanceHistory[mEqualizerPointer] = equalizerBalance; + mEqualizerGainHistory[mEqualizerPointer++] = (equalizerGain - 1.0f); + mEqualizerPointer %= mEqualizerHistorySize; + //Clear the next/upcoming values to give visual indication of where we're at. + mEqualizerBalanceHistory[mEqualizerPointer] = 0f; + mEqualizerGainHistory[mEqualizerPointer] = 0f; + + Platform.runLater(() -> { + XYChart.Series sampleSeries = new XYChart.Series<>(); + sampleSeries.setName("Demodulated Samples"); + + for(int x = 0; x < samples.length; x++) + { + sampleSeries.getData().add(new XYChart.Data<>(x, samples[x])); + } + + mSampleChart.getData().clear(); + mSampleChart.getData().add(sampleSeries); + + XYChart.Series patternSeries = new XYChart.Series<>(); + patternSeries.setName("Ideal Sync Pattern"); + + for(int x = 0; x < sync.length; x++) + { + patternSeries.getData().add(new XYChart.Data<>(syncIntervals[x], sync[x])); + } + + mSampleChart.getData().add(patternSeries); + + mPatternLabel.setText(label); + mEqualizerChart.getData().clear(); + + XYChart.Series balanceSeries = new XYChart.Series<>(); + balanceSeries.setName("Balance"); + XYChart.Series gainSeries = new XYChart.Series<>(); + gainSeries.setName("Gain (Value minus 1.0)"); + + for(int x = 0; x < mEqualizerHistorySize; x++) + { + balanceSeries.getData().add(new XYChart.Data<>(x, mEqualizerBalanceHistory[x])); + gainSeries.getData().add(new XYChart.Data<>(x, mEqualizerGainHistory[x])); + } + + mEqualizerChart.getData().add(balanceSeries); + mEqualizerChart.getData().add(gainSeries); + }); + } +} diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java index 8ad5ec68..47dce496 100644 --- a/src/main/java/io/github/dsheirer/identifier/Form.java +++ b/src/main/java/io/github/dsheirer/identifier/Form.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,6 +30,7 @@ public enum Form ARS_USER, ARS_PASSWORD, CALL_PROGRESS_TONE, + CATEGORY, CHANNEL, CHANNEL_DESCRIPTOR, CHANNEL_NAME, @@ -49,6 +50,7 @@ public enum Form NETWORK_ACCESS_CODE, PATCH_GROUP, RADIO, + RAN, RF_SUBSYSTEM, SCRAMBLE_PARAMETERS, SHORT_DATA_MESSAGE, diff --git a/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java b/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java index e9aae49c..38e82153 100644 --- a/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * * This 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,12 +14,12 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.identifier.configuration; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; @@ -31,12 +30,12 @@ import io.github.dsheirer.protocol.Protocol; * Configuration identifier that identifies a channel descriptor. This is primarily used to broadcast the channel * descriptor for a traffic channel so that it is aware of what channel number and frequency it is decoding. */ -public class ChannelDescriptorConfigurationIdentifier extends Identifier +public class ChannelDescriptorConfigurationIdentifier extends Identifier { /** * Constructs a channel descriptor configuration identifier */ - public ChannelDescriptorConfigurationIdentifier(IChannelDescriptor value) + public ChannelDescriptorConfigurationIdentifier(IBandChannelDescriptor value) { super(value, IdentifierClass.CONFIGURATION, Form.CHANNEL_DESCRIPTOR, Role.ANY); } diff --git a/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java b/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java index f14affd8..bd110c30 100644 --- a/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java @@ -30,7 +30,9 @@ import io.github.dsheirer.protocol.Protocol; public class EncryptionKeyIdentifier extends Identifier { - public EncryptionKeyIdentifier(EncryptionKey value, IdentifierClass identifierClass, Form form, Role role) + private Protocol mProtocol; + + public EncryptionKeyIdentifier(Protocol protocol, EncryptionKey value, IdentifierClass identifierClass, Form form, Role role) { super(value, identifierClass, form, role); } @@ -38,7 +40,7 @@ public class EncryptionKeyIdentifier extends Identifier @Override public Protocol getProtocol() { - return Protocol.APCO25; + return mProtocol; } public boolean isEncrypted() @@ -46,8 +48,8 @@ public class EncryptionKeyIdentifier extends Identifier return getValue() != null && getValue().isEncrypted(); } - public static EncryptionKeyIdentifier create(EncryptionKey encryptionKey) + public static EncryptionKeyIdentifier create(Protocol protocol, EncryptionKey encryptionKey) { - return new EncryptionKeyIdentifier(encryptionKey, IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY); + return new EncryptionKeyIdentifier(protocol, encryptionKey, IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY); } } diff --git a/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java b/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java deleted file mode 100644 index 8c768bf8..00000000 --- a/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General 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.carrier; - -import io.github.dsheirer.buffer.FloatAveragingBuffer; -import io.github.dsheirer.dsp.window.WindowFactory; -import io.github.dsheirer.dsp.window.WindowType; -import io.github.dsheirer.sample.complex.ComplexSamples; -import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter; -import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; -import org.jtransforms.fft.FloatFFT_1D; - -/** - * Calculates the carrier offset from channel center. Processes incoming Complex Sample buffers to detect the carrier - * signal and calculate a carrier offset measurement. - *

- * Calculates once a second by performing a 128-point FFT on each consecutive complex buffer. Once it detects a - * sequence of 5 consecutive buffers with sufficient SNR (>15 dB), it calculates the average offset across those 5 - * buffers and then averages across the 10 most recent high-SNR sequences. - */ -public class CarrierOffsetProcessor -{ - private static final int FFT_BIN_SIZE = 128; - private static final int MEASUREMENT_COUNT_THRESHOLD = 5; - private static final int SEARCH_WINDOW_MIDDLE = FFT_BIN_SIZE / 2; - private static final long CALCULATION_TIME_INTERVAL_MS = 1000; - private static final float CENTER_INDEX = FFT_BIN_SIZE / 2.0f - 1; - private static final float[] WINDOW = WindowFactory.getWindow(WindowType.BLACKMAN_HARRIS_7, FFT_BIN_SIZE * 2); - private static final FloatFFT_1D FFT = new FloatFFT_1D(FFT_BIN_SIZE); - private final FloatAveragingBuffer mAveragingBuffer = new FloatAveragingBuffer(5); - private final FloatAveragingBuffer mOffsetAverage = new FloatAveragingBuffer(10, 3); - private final StandardDeviation mStandardDeviation = new StandardDeviation(); - private float mCarrierOffset; - private float mResolution; - private int mHalfWidth; - private int mMeasurementCount = 0; - private int mSearchWindowMin; - private int mSearchWindowMax; - private long mEstimatedOffset; - private long mLastCalculationTimestamp = 0; - - /** - * Constructs an instance. - */ - public CarrierOffsetProcessor() - { - setSampleRate(50000.0); - } - - /** - * Update the current channel sample rate. - * @param sampleRate in Hertz - */ - public void setSampleRate(double sampleRate) - { - mResolution = (float)sampleRate / FFT_BIN_SIZE; - mHalfWidth = (int)Math.floor(12500.0 / mResolution / 2.0); //Half of 12.5 kHz signal width - mSearchWindowMin = SEARCH_WINDOW_MIDDLE - mHalfWidth; - mSearchWindowMax = SEARCH_WINDOW_MIDDLE + mHalfWidth; - } - - /** - * Resets this processor whenever the source tuner PPM value is changed or updated, so that we can immediately - * recalculate the carrier offset with the updated settings. - */ - public void reset() - { - mMeasurementCount = 0; - mLastCalculationTimestamp = 0; - mEstimatedOffset = 0; - mAveragingBuffer.reset(); - mStandardDeviation.clear(); - mOffsetAverage.reset(); - } - - /** - * Estimated carrier frequency offset averaged across buffers that have sufficient SNR (>15 dB). - * @return estimated average carrier offset. Note: this method can be accessed after each invocation of process() - * method. - */ - public long getEstimatedOffset() - { - return mEstimatedOffset; - } - - /** - * Indicates if a signal was detected in the most recently processed sample buffer. A value of false indicates the - * signal was either not present, or the SNR was less than 15 dB and therefore this processor could not calculate - * the carrier offest estimation. - * @return true if the most recent buffer had a signal that was also strong enough to process. - */ - public boolean hasCarrier() - { - return mCarrierOffset != Float.MAX_VALUE; - } - - /** - * Processes the complex sample buffer by calculating the detected carrier offset and mixing the buffer samples by - * remove the calculated offset. The current detected carrier offset is available via the getEstimatedOffset() - * method. - * - * @return true if the estimated carrier offset value was updated. - */ - public boolean process(ComplexSamples samples) - { - if(mLastCalculationTimestamp + CALCULATION_TIME_INTERVAL_MS < samples.timestamp()) - { - int bufferOffset = 0; - - while((bufferOffset + FFT_BIN_SIZE) <= samples.length()) - { - mCarrierOffset = calculateCarrierOffset(samples, bufferOffset); - - //Float max value indicates no signal or low SNR - reset the counter so that we can keep trying. - if(mCarrierOffset == Float.MAX_VALUE) - { - mMeasurementCount = 0; - mStandardDeviation.clear(); - } - else - { - mMeasurementCount++; - mAveragingBuffer.add(mCarrierOffset); - } - - if(mMeasurementCount >= MEASUREMENT_COUNT_THRESHOLD) - { - if(mStandardDeviation.getResult() < 5.0) - { - mOffsetAverage.add(mAveragingBuffer.getAverage()); - mLastCalculationTimestamp = samples.timestamp(); - - //Set the buffer offset to the sample length to exit the loop - bufferOffset = samples.length(); - } - - mMeasurementCount = 0; - mStandardDeviation.clear(); - } - - bufferOffset += FFT_BIN_SIZE; - } - - mEstimatedOffset = (long)mOffsetAverage.getAverage(); - return true; - } - - return false; - } - - /** - * Calculate the detected peak signal offset from the sample buffer. - * @param complexSamples to process - * @param offset into the complex samples buffer to calculate - * @return estimated carrier offset from center of the channel. - */ - private float calculateCarrierOffset(ComplexSamples complexSamples, int offset) - { - float[] samples = complexSamples.toInterleaved(offset, FFT_BIN_SIZE).samples(); - WindowFactory.apply(WINDOW, samples); - FFT.complexForward(samples); - float[] magnitudes = ComplexDecibelConverter.convert(samples); - float peakIndex = findPeak(magnitudes); - - //Return float max value for Low SNR or no signal - if(peakIndex == Float.MAX_VALUE) - { - return Float.MAX_VALUE; - } - - mStandardDeviation.increment(peakIndex); - return mResolution * (peakIndex - CENTER_INDEX); - } - - /** - * Finds the FFT bin with the largest magnitude within a window of +/- 12.5 kHz of the center bin. - * @param magnitudes from the FFT measurement. - * @return center bin or Float.MAX_VALUE to indicate no signal or low SNR. - */ - private float findPeak(float[] magnitudes) - { - float peakValue = -150.0f; - int peakIndex = 0; - int min = mSearchWindowMin; - int max = mSearchWindowMax; - int half = mHalfWidth; - - //Find the peak magnitude within twice the search window width of the center indices. - for(int x = min; x <= max; x++) - { - if(magnitudes[x] > peakValue) - { - peakValue = magnitudes[x]; - peakIndex = x; - } - } - - //From there, find the center point that best fits the signal to the search window width. We need a minimum - // SNR for this to work. - int left = peakIndex - half; - int right = peakIndex + half; - - //Do we have enough SNR? - float snr = peakValue - ((magnitudes[left] + magnitudes[right]) / 2.0f); - - if(snr > 15.0f) - { - float minimumSearchThreshold = peakValue - 16.0f; //Stop at bin values 16 db lower. - - while(magnitudes[left] < minimumSearchThreshold && left < peakIndex) - { - left++; - } - - while(magnitudes[right] < minimumSearchThreshold && right > peakIndex) - { - right--; - } - - int binWidth = right - left; - - if(binWidth == 0) - { - return peakIndex; - } - else - { - return left + (binWidth / 2.0f); - } - } - - //No signal or low SNR - return MAX VALUE to indicate no signal. - return Float.MAX_VALUE; - } -} 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 64aa2b9c..8f7333dc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -81,6 +81,12 @@ import io.github.dsheirer.module.decode.mpt1327.Sync; import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM; import io.github.dsheirer.module.decode.nbfm.NBFMDecoder; import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState; +import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN; +import io.github.dsheirer.module.decode.nxdn.NXDNDecoder; +import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState; +import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager; +import io.github.dsheirer.module.decode.nxdn.audio.NXDNAudioModule; +import io.github.dsheirer.module.decode.nxdn.layer3.filter.NXDNMessageFilterSet; import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager; import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule; import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule; @@ -171,9 +177,6 @@ public class DecoderFactory processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig, trafficChannelManager, channelDescriptor); break; - case NBFM: - processNBFM(channel, modules, aliasList, decodeConfig); - break; case LTR: processLTRStandard(channel, modules, aliasList, (DecodeConfigLTRStandard) decodeConfig); break; @@ -184,6 +187,12 @@ public class DecoderFactory processMPT1327(channelMapModel, channel, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig, userPreferences); break; + case NBFM: + processNBFM(channel, modules, aliasList, decodeConfig); + break; + case NXDN: + processNXDN(channel, userPreferences, modules, aliasList, decodeConfig, trafficChannelManager, channelDescriptor); + break; case PASSPORT: processPassport(channel, modules, aliasList, decodeConfig); break; @@ -462,6 +471,55 @@ public class DecoderFactory } } + /** + * Creates decoder modules for the NXDN decoder. + * @param channel configuration with center frequency + * @param modules to receive the decoder modules + * @param aliasList for aliases. + * @param decodeConfig with details + */ + private static void processNXDN(Channel channel, UserPreferences userPreferences, List modules, + AliasList aliasList, DecodeConfiguration decodeConfig, + TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor) + { + if(decodeConfig instanceof DecodeConfigNXDN configNXDN) + { + //Add a channel rotation monitor when we have multiple control channel frequencies specified + if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf && + sctmf.hasMultipleFrequencies()) + { + List activeStates = new ArrayList<>(); + activeStates.add(State.CONTROL); + modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences)); + } + + modules.add(new NXDNDecoder(configNXDN)); + modules.add(new NXDNAudioModule(userPreferences, aliasList)); + + if(channel.getChannelType() == ChannelType.STANDARD) + { + NXDNTrafficChannelManager primaryTCM = new NXDNTrafficChannelManager(channel); + modules.add(primaryTCM); + modules.add(new NXDNDecoderState(channel, primaryTCM)); + } + else if(trafficChannelManager instanceof NXDNTrafficChannelManager parentTCM) + { + NXDNDecoderState decoderState = new NXDNDecoderState(channel, parentTCM); + decoderState.setCurrentChannel(channelDescriptor); + modules.add(decoderState); + } + else + { + mLog.warn("Expected non-null NXDN traffic channel manager for channel " + channel.getName()); + } + + } + else + { + throw new IllegalArgumentException("Can't create NXDN decoder - unrecognized config: " + decodeConfig); + } + } + /** * Creates modules for DMR decoder setup. * @@ -622,9 +680,9 @@ public class DecoderFactory for(Module module : modules) { - if(module instanceof Decoder) + if(module instanceof Decoder decoder) { - filterSet.addFilters(getMessageFilter(((Decoder)module).getDecoderType())); + filterSet.addFilters(getMessageFilter(decoder.getDecoderType())); } } @@ -668,6 +726,9 @@ public class DecoderFactory case MPT1327: filters.add(new MPT1327MessageFilter()); break; + case NXDN: + filters.add(new NXDNMessageFilterSet()); + break; case P25_PHASE1: filters.add(new P25P1MessageFilterSet()); break; @@ -708,6 +769,8 @@ public class DecoderFactory return new DecodeConfigMPT1327(); case NBFM: return new DecodeConfigNBFM(); + case NXDN: + return new DecodeConfigNXDN(); case PASSPORT: return new DecodeConfigPassport(); case P25_PHASE1: @@ -767,6 +830,13 @@ public class DecoderFactory copyNBFM.setSquelchNoiseCloseThreshold(origNBFM.getSquelchNoiseCloseThreshold()); copyNBFM.setTalkgroup(origNBFM.getTalkgroup()); return copyNBFM; + case NXDN: + DecodeConfigNXDN origNXDN = (DecodeConfigNXDN)config; + DecodeConfigNXDN copyNXDN = new DecodeConfigNXDN(); + copyNXDN.setChannelMap(origNXDN.getChannelMap()); + copyNXDN.setTransmissionMode(origNXDN.getTransmissionMode()); + copyNXDN.setTrafficChannelPoolSize(origNXDN.getTrafficChannelPoolSize()); + return copyNXDN; case P25_PHASE1: DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config; DecodeConfigP25Phase1 copyP25 = new DecodeConfigP25Phase1(); 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 02311d9c..5dcfafed 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,11 +28,12 @@ public enum DecoderType { //Primary Decoders AM("AM", "AM", Protocol.UNKNOWN), - DMR("DMR","DMR", Protocol.DMR), + DMR("DMR", "DMR", Protocol.DMR), LTR("LTR", "LTR", Protocol.LTR), LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET), MPT1327("MPT1327", "MPT1327", Protocol.MPT1327), NBFM("NBFM", "NBFM", Protocol.NBFM), + NXDN("NXDN", "NXDN", Protocol.NXDN), PASSPORT("Passport", "Passport", Protocol.PASSPORT), P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25), P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2), @@ -44,9 +45,9 @@ public enum DecoderType MDC1200("MDC1200", "MDC1200", Protocol.MDC1200), TAIT_1200("Tait 1200", "Tait 1200", Protocol.TAIT1200); - private String mDisplayString; - private String mShortDisplayString; - private Protocol mProtocol; + private final String mDisplayString; + private final String mShortDisplayString; + private final Protocol mProtocol; DecoderType(String displayString, String shortDisplayString, Protocol protocol) { @@ -58,38 +59,23 @@ public enum DecoderType /** * Primary decoders that operate on I/Q sample streams */ - public static EnumSet PRIMARY_DECODERS = - EnumSet.of(DecoderType.AM, - DecoderType.DMR, - DecoderType.LTR, - DecoderType.LTR_NET, - DecoderType.MPT1327, - DecoderType.NBFM, - DecoderType.P25_PHASE1, - DecoderType.P25_PHASE2, - DecoderType.PASSPORT); + public static final EnumSet PRIMARY_DECODERS = EnumSet.of(AM, DMR, LTR, LTR_NET, MPT1327, NBFM, NXDN, + P25_PHASE1, P25_PHASE2, PASSPORT); /** * Auxiliary decoders that operate on in-band signalling in the decoded audio channel */ - public static final EnumSet AUX_DECODERS = - EnumSet.of(DecoderType.DCS, - DecoderType.FLEETSYNC2, - DecoderType.LJ_1200, - DecoderType.MDC1200, - DecoderType.TAIT_1200); + public static final EnumSet AUX_DECODERS = EnumSet.of(DCS, FLEETSYNC2, LJ_1200, MDC1200, TAIT_1200); /** * Decoders that produce a (recordable) bitstream */ - public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR, - DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2); + public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DMR, MPT1327, NXDN, P25_PHASE1, P25_PHASE2); /** * Decoders that produce (recordable) MBE audio codec frames */ - public static final EnumSet MBE_AUDIO_CODEC_DECODERS = - EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2); + public static final EnumSet MBE_AUDIO_CODEC_DECODERS = EnumSet.of(DMR, NXDN, P25_PHASE1, P25_PHASE2); public Protocol getProtocol() { diff --git a/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java b/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java index 7aa5c283..c3cdf8d7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,8 +29,19 @@ import io.github.dsheirer.source.SourceEvent; */ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceEventProvider { + private static final double TWO_PI = 2 * Math.PI; private Listener mSourceEventListener; private Listener mSymbolListener; + private double mSampleRate; + + /** + * Sets the decimated final sample rate that will be used for reporting decoder PLL errors. + * @param rate in Hertz + */ + protected void setDecimatedSampleRate(double rate) + { + mSampleRate = rate; + } /** * Protocol description suitable for display in the user interface @@ -68,13 +79,19 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE } /** - * Processes the current phase-locked loop (PLL) measurement and sends to the parent tuner for self correction. - * @param pllError as measured in the decoder. + * Processes the current phase-locked loop (PLL) measurement and sends to the channel frequency error manager to + * correct the error by mixing the incoming sample stream to remove the error. + * + * Note: this source event error measurement is rebroadcast by the ProcessingChain and received by the + * ChannelFrequencyErrorManager that lives within the TunerChannelSource where it adjusts the channel's mixer to + * incrementally correct and drive the error toward zero in concert with the parent tuner's auto PPM correction. + * + * @param pllError as measured in the decoder, in Radians. */ - public void processPLLError(float pllError, int baudRate) + public void processPLLError(float pllError) { - long frequencyError = (long)(baudRate / 2.0 * (pllError / Math.PI)); - broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(frequencyError, "Decoder measured error sync locked")); + long correctionRequestHertz = (long)(mSampleRate / TWO_PI * pllError); + broadcast(SourceEvent.frequencyCorrectionRequest(correctionRequestHertz)); } /** @@ -105,4 +122,14 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE mSymbolListener.receive(symbol); } } + + public String getEqualizerSettings() + { + return "Not implemented"; + } + + public void resetEqualizer() + { + //No-op, unless implemented in subclass. + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java index b1d579c8..f7f1d180 100644 --- a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ */ package io.github.dsheirer.module.decode.analog; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.channel.state.DecoderState; import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.channel.state.State; @@ -42,7 +42,7 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class); private Listener mSourceEventListener = new SourceEventListener(); private DecodeEvent mDecodeEvent; - private IChannelDescriptor mChannelDescriptor = null; + private IBandChannelDescriptor mChannelDescriptor = null; public AnalogDecoderState() { 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 9b45da7f..e25f26e9 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,6 +31,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet; import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard; import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327; import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM; +import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN; import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25; import io.github.dsheirer.module.decode.passport.DecodeConfigPassport; import io.github.dsheirer.source.tuner.channel.ChannelSpecification; @@ -39,13 +40,14 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DecodeConfigAM.class, name = "decodeConfigAM"), + @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR"), @JsonSubTypes.Type(value = DecodeConfigLTRNet.class, name = "decodeConfigLTRNet"), @JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"), @JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"), @JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"), + @JsonSubTypes.Type(value = DecodeConfigNXDN.class, name = "decodeConfigNXDN"), @JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"), - @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"), - @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR") + @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport") }) @JacksonXmlRootElement(localName = "decode_configuration") public abstract class DecodeConfiguration extends Configuration @@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration { return new int[0]; } + + @JsonIgnore + public int getBitRate() + { + return getDecoderType().getProtocol().getBitRate(); + } } 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 index 51982a91..49502e4b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,10 +28,8 @@ import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory; import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat; -import io.github.dsheirer.dsp.squelch.PowerMonitor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; -import io.github.dsheirer.module.carrier.CarrierOffsetProcessor; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.FeedbackDecoder; import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule; @@ -48,7 +46,6 @@ import io.github.dsheirer.sample.buffer.IByteBufferProvider; import io.github.dsheirer.sample.complex.ComplexSamples; import io.github.dsheirer.sample.complex.IComplexSamplesListener; import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.ISourceEventProvider; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.wave.ComplexWaveSource; import java.io.File; @@ -82,8 +79,8 @@ import org.slf4j.LoggerFactory; * The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and * embedded link control parameters. */ -public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener, - ISourceEventProvider, Listener +public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, + ISourceEventListener, Listener { private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class); private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); @@ -100,8 +97,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, private IRealDecimationFilter mDecimationFilterQ; private RealFIRFilter mRRCFilterI; private RealFIRFilter mRRCFilterQ; - private final PowerMonitor mPowerMonitor = new PowerMonitor(); - private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor(); /** * Constructs an instance @@ -140,9 +135,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, SYMBOL_RATE + " symbol rate)"); } - mPowerMonitor.setSampleRate((int)sampleRate); - mCarrierOffsetProcessor.setSampleRate(sampleRate); - mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate)); mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate)); @@ -158,6 +150,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation); float decimatedSampleRate = (float)sampleRate / decimation; + + //Set the decimated sample rate to use for PLL error reporting. + setDecimatedSampleRate(decimatedSampleRate); + float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate)); int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33); symbolLength += symbolLength % 2; //Make the symbol length even @@ -190,9 +186,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, float[] i = mIBasebandFilter.filter(samples.i()); float[] q = mQBasebandFilter.filter(samples.q()); - //Process buffer for power measurements - mPowerMonitor.process(i, q); - i = mDecimationFilterI.decimateReal(i); q = mDecimationFilterQ.decimateReal(q); @@ -201,25 +194,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, float[] demodulated = mDemodulator.demodulate(i, q); mSymbolProcessor.receive(demodulated); - - //Estimate carrier offset and broadcast at each update. This value is used in the channel spectral display, - // and it's also processed by the tuner's PPM error monitor to auto-adjust the tuner PPM value. - if(mCarrierOffsetProcessor.process(samples)) - { - //Tuner PPM Monitor - negate the value to indicate channel error from tuner's PPM that's causing the offset - mPowerMonitor.broadcast(SourceEvent.frequencyErrorMeasurement(-mCarrierOffsetProcessor.getEstimatedOffset())); - - //Channel spectral display - when there's a carrier send the estimate, otherwise send a zero to cause the - //display to blank the carrier offset measurement indicator line - if(mCarrierOffsetProcessor.hasCarrier()) - { - mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(mCarrierOffsetProcessor.getEstimatedOffset())); - } - else - { - mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(0)); - } - } } /** @@ -299,21 +273,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, return "DMR C4FM"; } - /** - * Sets the source event listener to receive source events from this decoder. - */ - @Override - public void setSourceEventListener(Listener listener) - { - mPowerMonitor.setSourceEventListener(listener); - } - - @Override - public void removeSourceEventListener() - { - mPowerMonitor.setSourceEventListener(null); - } - @Override public void start() { @@ -340,9 +299,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, case NOTIFICATION_SAMPLE_RATE_CHANGE: setSampleRate(sourceEvent.getValue().doubleValue()); break; - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - mCarrierOffsetProcessor.reset(); - break; } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java index 93913897..19251a86 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2025 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -67,7 +67,6 @@ public class DMRSoftSymbolProcessor private static final int BUFFER_PROTECTED_REGION_DIBITS = 92; private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS; - private static final int SYMBOL_RATE = 4800; private static final float EQUALIZER_LOOP_GAIN = 0.15f; private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0); private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f; @@ -565,7 +564,7 @@ public class DMRSoftSymbolProcessor mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE); mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE); - mFeedbackDecoder.processPLLError(mEqualizerBalance, SYMBOL_RATE); + mFeedbackDecoder.processPLLError(mEqualizerBalance); //Constrain gain between 1.0f and 1.35f mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN); 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 index f463cf02..67441e5d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.dmr; import com.google.common.eventbus.Subscribe; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.Channel.ChannelType; import io.github.dsheirer.controller.channel.ChannelConversionRequest; @@ -225,7 +225,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I * @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, + public void convertToTrafficChannel(Channel channel, long currentFrequency, IBandChannelDescriptor restChannel, DMRNetworkConfigurationMonitor networkConfigurationMonitor) { mLock.lock(); 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 index 970d6c94..65e586d9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -162,7 +162,7 @@ public class DecodeConfigDMR extends DecodeConfiguration @Override public ChannelSpecification getChannelSpecification() { - return new ChannelSpecification(50000.0, 12500, 6500.0, 7200.0); + return new ChannelSpecification(24000.0, 12500, 6500.0, 7200.0); } /** 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 index da4917d3..41e874f5 100644 --- 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ package io.github.dsheirer.module.decode.dmr.channel; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; @@ -35,7 +35,7 @@ import org.apache.commons.lang3.Validate; * * Note: timeslots are tracked as 1 and 2 */ -public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver +public abstract class DMRChannel extends IntegerIdentifier implements IBandChannelDescriptor, ITimeslotFrequencyReceiver { private TimeslotFrequency mTimeslotFrequency; private int mTimeslot; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java similarity index 84% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java rename to src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java index 2999d549..03dc35b0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ * **************************************************************************** */ -package io.github.dsheirer.module.decode.p25; +package io.github.dsheirer.module.decode.event; import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.identifier.Form; @@ -28,33 +28,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic - * channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need - * for a new event. + * Base channel event tracker + * @param type of decode event being tracked. */ -public class P25TrafficChannelEventTracker +public abstract class ChannelEventTracker { - private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class); - private static final long STALE_EVENT_THRESHOLD_MS = 2000; + private static final Logger LOGGER = LoggerFactory.getLogger(ChannelEventTracker.class); + private static final long STALE_EVENT_THRESHOLD_MS = 3000; private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000; - private P25ChannelGrantEvent mEvent; private boolean mStarted = false; private boolean mComplete = false; + private final T mEvent; /** * Constructs an instance - * @param event to track for the traffic channel. + * @param event to be tracked */ - public P25TrafficChannelEventTracker(P25ChannelGrantEvent event) + public ChannelEventTracker(T event) { mEvent = event; } /** * Access the underlying traffic channel event that is being tracked. + * * @return event. */ - public P25ChannelGrantEvent getEvent() + public T getEvent() { return mEvent; } @@ -62,6 +62,7 @@ public class P25TrafficChannelEventTracker /** * Indicates if this event is stale relative to the provided timestamp. Staleness is determined by the time delta * between the event's end time and timestamp argument. + * * @param timestamp to check for staleness * @return true if the delta time exceeds a threshold. */ @@ -76,23 +77,23 @@ public class P25TrafficChannelEventTracker } /** - * Indicates if the TDMA data channel duration exceeds the threshold (15 seconds) + * Indicates if the data channel duration exceeds the threshold (15 seconds) */ - public boolean exceedsMaxTDMADataDuration() + public boolean exceedsMaxDataDuration() { return getEvent().getDuration() > MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS; } /** * Adds the identifier to the tracked event if the event's identifier collection does not already have it. + * * @param identifier to add */ public void addIdentifierIfMissing(Identifier identifier) { if(identifier != null && !getEvent().getIdentifierCollection().hasIdentifier(identifier)) { - MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection() - .getIdentifiers()); + MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection().getIdentifiers()); mic.update(identifier); getEvent().setIdentifierCollection(mic); } @@ -101,7 +102,8 @@ public class P25TrafficChannelEventTracker /** * Adds the additional details to the tracked event if the current event details are null or if they do not contain * the additional details. - * @param additionalDetails + * + * @param additionalDetails for the event */ public void addDetailsIfMissing(String additionalDetails) { @@ -135,6 +137,7 @@ public class P25TrafficChannelEventTracker /** * Indicates if the tracked event from identifier is non null and that it is different to the from argument. + * * @param fromToCompare against the current event from identifier. * @return true if they are different. */ @@ -153,6 +156,7 @@ public class P25TrafficChannelEventTracker /** * Checks the call for staleness and verifies that the call event TO identifier is equal to the TO identifier * in the provided identifier collection. + * * @param toCompareIC containing a TO and optionally a FROM identifier to compare. * @param timestamp to check for staleness * @return true if the call identifiers are the same and the call is not stale. @@ -195,6 +199,7 @@ public class P25TrafficChannelEventTracker /** * Indicates if the event has been marked as complete by traffic channel signalling. + * * @return complete status. */ public boolean isComplete() @@ -204,6 +209,7 @@ public class P25TrafficChannelEventTracker /** * Indicates if the event has been marked as started by the traffic channel for HDU or LDU signalling. + * * @return started status. */ public boolean isStarted() @@ -236,6 +242,7 @@ public class P25TrafficChannelEventTracker * * Note: once the event is being updated from the traffic channel, any attempts to update from the control channel * are ignored. + * * @param timestamp to assign. */ public void updateDurationTraffic(long timestamp) @@ -255,6 +262,7 @@ public class P25TrafficChannelEventTracker * Mark the event as complete and assign final end timestamp to the event. * * Note: further attempts to complete an already complete event are ignored. + * * @param timestamp to assign. * @return true if the timestamp was updated */ @@ -272,6 +280,7 @@ public class P25TrafficChannelEventTracker /** * Updates the details for the tracked event. + * * @param details to update */ public void setDetails(String details) @@ -279,8 +288,46 @@ public class P25TrafficChannelEventTracker getEvent().setDetails(details); } + /** + * Prefixes the details the current event details text + * @param details to prefix + */ + public void prefixDetails(String details) + { + if(getEvent().getDetails() == null) + { + getEvent().setDetails(details); + } + else + { + if(!getEvent().getDetails().contains(details)) + { + getEvent().setDetails(details + " " + getEvent().getDetails()); + } + } + } + + /** + * Adds the details to the end of the existing event text or sets the event details. + */ + public void addDetails(String details) + { + if(getEvent().getDetails() == null) + { + getEvent().setDetails(details); + } + else + { + if(!getEvent().getDetails().contains(details)) + { + getEvent().setDetails(getEvent().getDetails() + " " + details); + } + } + } + /** * Updates the channel descriptor for the tracked event. + * * @param channelDescriptor to update. */ public void addChannelDescriptorIfMissing(IChannelDescriptor channelDescriptor) diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java index 68f35a95..f088bdbd 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java index ff0497da..b6de52ed 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -101,12 +101,19 @@ public class DecodeEventDuplicateDetector } /** - * Generates a unique event key that is a combinatio of the TO identifier value and the details of the event. + * Generates a unique event key that is a combination of the TO identifier value and the details of the event. * @param event for the key * @return generated key. */ private static String getKey(IDecodeEvent event) { + if(event.getIdentifierCollection().getFromIdentifier() != null) + { + return event.getIdentifierCollection().getToIdentifier().toString() + + event.getIdentifierCollection().getFromIdentifier().toString() + + event.getDetails(); + } + return event.getIdentifierCollection().getToIdentifier().toString() + event.getDetails(); } 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 82be10c4..0d54ce7d 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by 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 55609355..113ffea4 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2026 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,24 +59,24 @@ import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; +/** + * Decode event table view + */ public class DecodeEventPanel extends JPanel implements Listener { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger(DecodeEventPanel.class); private static final String TABLE_PREFERENCE_KEY = "decode.event.panel"; - private JTable mTable; - private JTableColumnWidthMonitor mTableColumnWidthMonitor; - private DecodeEventModel mEventModel = new DecodeEventModel(); + private final JTable mTable; + private final DecodeEventModel mEventModel = new DecodeEventModel(); private DecodeEventHistory mCurrentEventHistory; - private JScrollPane mEmptyScroller; - private IconModel mIconModel; - private AliasModel mAliasModel; - private UserPreferences mUserPreferences; - private TimestampCellRenderer mTimestampCellRenderer; - private FilterSet mFilterSet = new DecodeEventFilterSet(); - private TableRowSorter mTableRowSorter; - private HistoryManagementPanel mHistoryManagementPanel; + private final IconModel mIconModel; + private final AliasModel mAliasModel; + private final UserPreferences mUserPreferences; + private final TimestampCellRenderer mTimestampCellRenderer; + private final FilterSet mFilterSet = new DecodeEventFilterSet(); + private final HistoryManagementPanel mHistoryManagementPanel; /** @@ -93,19 +93,19 @@ public class DecodeEventPanel extends JPanel implements Listener(mEventModel); - mTableRowSorter.setRowFilter(new EventRowFilter()); - mTable.setRowSorter(mTableRowSorter); - mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY); + TableRowSorter tableRowSorter = new TableRowSorter<>(mEventModel); + tableRowSorter.setRowFilter(new EventRowFilter()); + mTable.setRowSorter(tableRowSorter); + JTableColumnWidthMonitor tableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY); updateCellRenderers(); mHistoryManagementPanel = new HistoryManagementPanel<>(mEventModel, "Event Filter Editor"); mHistoryManagementPanel.updateFilterSet(mFilterSet); add(mHistoryManagementPanel, "span,growx"); - mEmptyScroller = new JScrollPane(mTable); - add(mEmptyScroller); + JScrollPane emptyScroller = new JScrollPane(mTable); + add(emptyScroller); //Register filter change listener to refresh the table any time the event filters are changed. - mFilterSet.register(() -> mEventModel.fireTableDataChanged()); + mFilterSet.register(mEventModel::fireTableDataChanged); } public void dispose() @@ -122,7 +122,7 @@ public class DecodeEventPanel extends JPanel implements Listener mTimestampCellRenderer.updatePreferences()); + EventQueue.invokeLater(mTimestampCellRenderer::updatePreferences); } } @@ -215,7 +215,7 @@ public class DecodeEventPanel extends JPanel implements Listener 0) + if(!sb.isEmpty()) { sb.append(","); } @@ -262,9 +262,8 @@ public class DecodeEventPanel extends JPanel implements Listener identifiers = identifierCollection.getIdentifiers(mRole); if(identifiers != null && !identifiers.isEmpty()) @@ -281,13 +280,13 @@ public class DecodeEventPanel extends JPanel implements Listener 0) + if(!sb.isEmpty()) { sb.append(","); } sb.append(Joiner.on(", ").skipNulls().join(aliases)); - color = aliases.get(0).getDisplayColor(); - icon = mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE); + color = aliases.getFirst().getDisplayColor(); + icon = mIconModel.getIcon(aliases.getFirst().getIconName(), IconModel.DEFAULT_ICON_SIZE); } } @@ -337,9 +336,9 @@ public class DecodeEventPanel extends JPanel implements Listener 0) @@ -409,7 +406,7 @@ public class DecodeEventPanel extends JPanel implements Listener 0) { diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java index b0fa9f52..e06a9bd8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * * This 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,16 +14,16 @@ * * 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.channel; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.protocol.Protocol; -public class LtrNetChannel implements IChannelDescriptor, Comparable +public class LtrNetChannel implements IBandChannelDescriptor, Comparable { private long mDownlink; private long mUplink; diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java index 14a894bf..69b69b3f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * * This 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,16 +14,16 @@ * * 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.ltrstandard.channel; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.protocol.Protocol; -public class LtrChannel implements IChannelDescriptor, Comparable +public class LtrChannel implements IBandChannelDescriptor, Comparable { private long mDownlink; private int mChannel; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java index 7e249d07..68712bc7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ */ package io.github.dsheirer.module.decode.mpt1327; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.module.decode.event.DecodeEventType; import io.github.dsheirer.protocol.Protocol; @@ -52,7 +52,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent protected long mDuration; protected DecodeEventType mDecodeEventType; protected IdentifierCollection mIdentifierCollection; - protected IChannelDescriptor mChannelDescriptor; + protected IBandChannelDescriptor mChannelDescriptor; protected String mDetails; /** @@ -90,7 +90,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent * Sets the channel descriptor for this event * @param channelDescriptor */ - public MPT1327ChannelGrantDecodeEventBuilder channel(IChannelDescriptor channelDescriptor) + public MPT1327ChannelGrantDecodeEventBuilder channel(IBandChannelDescriptor channelDescriptor) { mChannelDescriptor = channelDescriptor; return this; diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java index f6c75562..18d61f76 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * * This 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,22 +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.mpt1327.channel; -import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.protocol.Protocol; - import java.util.Objects; /** * MPT-1327 Channel */ -public class MPT1327Channel implements IChannelDescriptor +public class MPT1327Channel implements IBandChannelDescriptor { private int mChannelNumber; private ChannelMap mChannelMap; diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java new file mode 100644 index 00000000..7a907ef7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java @@ -0,0 +1,218 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + + +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.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +import io.github.dsheirer.source.tuner.channel.ChannelSpecification; +import java.util.ArrayList; +import java.util.List; + +/** + * NXDN decoder configuration + */ +public class DecodeConfigNXDN extends DecodeConfiguration +{ + public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 200; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500; + public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000; + private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; + private boolean mIgnoreDataCalls = false; + private boolean mIgnoreEncryptedCalls = false; + + private static final ChannelSpecification CHANNEL_4800 = new ChannelSpecification(12000.0, 6250, 3000.0, 3125.0); + private static final ChannelSpecification CHANNEL_9600 = new ChannelSpecification(24000.0, 12500, 5200.0, 6250.0); + private TransmissionMode mTransmissionMode = TransmissionMode.M4800; + private List mChannelMap = new ArrayList<>(); + private Encoding mEncoding = Encoding.UTF8; + + /** + * Default constructor for Jackson. + */ + public DecodeConfigNXDN() + { + } + + /** + * Override the parent method to indicate 4800 or 9600 bits per second, depending on the mode. + */ + @Override + public int getBitRate() + { + return getTransmissionMode().getSymbolRate() * 2; + } + + /** + * Constructs an instance + * @param transmissionMode for the system + */ + public DecodeConfigNXDN(TransmissionMode transmissionMode) + { + mTransmissionMode = transmissionMode; + } + + /** + * Optional user-provided channel to frequency mapping + * @return map + */ + @JacksonXmlProperty(localName = "channelMap") + public List getChannelMap() + { + return mChannelMap; + } + + /** + * Sets the optional user-provided channel to frequency mapping + * @param channelMap with entries to configure channels + */ + public void setChannelMap(List channelMap) + { + mChannelMap = channelMap; + } + + /** + * Adds the channel frequency entry to the map + * @param channelFrequency to add + */ + public void add(ChannelFrequency channelFrequency) + { + mChannelMap.add(channelFrequency); + } + + /** + * Ignore data calls + * @return true if ignore + */ + @JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls") + public boolean isIgnoreDataCalls() + { + return mIgnoreDataCalls; + } + + /** + * Set ignore data calls + * @param ignore data calls + */ + public void setIgnoreDataCalls(boolean ignore) + { + mIgnoreDataCalls = ignore; + } + + /** + * Ignore encrypted calls + * @return true if ignore + */ + @JacksonXmlProperty(isAttribute = true, localName = "ignore_encrypted_calls") + public boolean isIgnoreEncryptedCalls() + { + return mIgnoreEncryptedCalls; + } + + /** + * Set ignore encrypted calls + * @param ignore encrypted calls + */ + public void setIgnoreEncryptedCalls(boolean ignore) + { + mIgnoreEncryptedCalls = 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; + } + + /** + * Transmission mode for this system. + */ + @JacksonXmlProperty(isAttribute = true, localName = "mode") + public TransmissionMode getTransmissionMode() + { + return mTransmissionMode; + } + + /** + * Sets/changes the transmission mode + * @param transmissionMode to use + */ + public void setTransmissionMode(TransmissionMode transmissionMode) + { + mTransmissionMode = transmissionMode; + } + + /** + * Character set for encoding talker alias values + */ + @JacksonXmlProperty(isAttribute = true, localName = "charset") + public Encoding getEncoding() + { + return mEncoding; + } + + /** + * Sets the character set for talker alias encoding + * @param encoding to use + */ + public void setEncoding(Encoding encoding) + { + mEncoding = encoding; + } + + @JsonIgnore + @Override + public DecoderType getDecoderType() + { + return DecoderType.NXDN; + } + + @JsonIgnore + @Override + public ChannelSpecification getChannelSpecification() + { + return switch(getTransmissionMode()) + { + case M4800, TYPE_D -> CHANNEL_4800; + case M9600 -> CHANNEL_9600; + }; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java new file mode 100644 index 00000000..12ab79de --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java @@ -0,0 +1,60 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.module.decode.event.ChannelEventTracker; +import io.github.dsheirer.module.decode.event.DecodeEvent; + +/** + * Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic + * channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need + * for a new event. + */ +public class NXDNChannelEventTracker extends ChannelEventTracker +{ + private boolean mTrafficChannelAllocated = false; + + /** + * Constructs an instance + * + * @param event to track for the traffic channel. + */ + public NXDNChannelEventTracker(DecodeEvent event) + { + super(event); + } + + /** + * Indicates if a traffic channel was allocated for this event + */ + public boolean isTrafficChannelAllocated() + { + return mTrafficChannelAllocated; + } + + /** + * Sets flag to indicate if traffic channel was allocated + * @param allocated as the flag + */ + public void setTrafficChannelAllocated(boolean allocated) + { + mTrafficChannelAllocated = allocated; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java new file mode 100644 index 00000000..340c5ca9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java @@ -0,0 +1,43 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.controller.channel.event.PreloadDataContent; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation; +import java.util.List; + +/** + * Channel access info and optional channel frequencies to preload into a traffic channel. + */ +public class NXDNChannelInfoPreloadData extends PreloadDataContent +{ + /** + * Constructs an instance + * + * @param info from the channel to preload + * @param frequencies optional provided by the user in the config + */ + public NXDNChannelInfoPreloadData(ChannelAccessInformation info, List frequencies) + { + super(new ChannelInformation(info, frequencies)); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java new file mode 100644 index 00000000..a09b51c9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java @@ -0,0 +1,54 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.module.decode.event.DecodeEvent; +import io.github.dsheirer.module.decode.event.DecodeEventType; +import io.github.dsheirer.protocol.Protocol; + +/** + * NXDN Decode Event + */ +public class NXDNDecodeEvent extends DecodeEvent +{ + /** + * Constructs a decode event + * @param decodeEventType for the event + * @param start timestamp + */ + public NXDNDecodeEvent(DecodeEventType decodeEventType, long start) + { + super(decodeEventType, start); + setProtocol(Protocol.NXDN); + } + + /** + * Creates a new decode event builder with the specified start timestamp. + * @param timeStart for the event + * @return builder + */ + public static DecodeEventBuilder builder(DecodeEventType decodeEventType, long timeStart) + { + DecodeEventBuilder decodeEventBuilder = new DecodeEventBuilder(decodeEventType, timeStart); + decodeEventBuilder.protocol(Protocol.NXDN); + decodeEventBuilder.timeslot(0); + return decodeEventBuilder; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java new file mode 100644 index 00000000..edff4637 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java @@ -0,0 +1,413 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory; +import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; +import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; +import io.github.dsheirer.dsp.fm.FmDemodulatorFactory; +import io.github.dsheirer.dsp.fm.IDemodulator; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.IByteBufferProvider; +import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.sample.complex.IComplexSamplesListener; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.tuner.channel.ChannelSpecification; +import io.github.dsheirer.source.wave.ComplexWaveSource; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NXDN Decoder for 4800, 9600 and Type-C/D systems. + */ +public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, + ISourceEventListener, Listener +{ + private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class); + private static final Map BASEBAND_FILTERS = new HashMap<>(); + private final DecodeConfigNXDN mConfig; + private final NXDNSymbolProcessor mSymbolProcessor; + private final NXDNMessageFramer mMessageFramer; + private final NXDNMessageProcessor mMessageProcessor; + private IRealDecimationFilter mDecimationFilterI; + private IRealDecimationFilter mDecimationFilterQ; + private IRealFilter mBasebandFilterI; + private IRealFilter mBasebandFilterQ; + private IRealFilter mPulseShapingFilter; + private IDemodulator mDemodulator; + + @Override + public DecoderType getDecoderType() + { + return DecoderType.NXDN; + } + + /** + * Constructs an instance + * @param config for the decoder + */ + public NXDNDecoder(DecodeConfigNXDN config) + { + mConfig = config; + mMessageProcessor = new NXDNMessageProcessor(config); + mMessageProcessor.setMessageListener(getMessageListener()); + mMessageFramer = new NXDNMessageFramer(mMessageProcessor, config.getTransmissionMode()); + mSymbolProcessor = new NXDNSymbolProcessor(mMessageFramer, this); + } + + @Override + public void resetEqualizer() + { + mSymbolProcessor.resetEqualizer(); + } + + @Override + public String getEqualizerSettings() + { + return mSymbolProcessor.getEqualizerSettings(); + } + + /** + * Preloads NXDN channel details passed from the control channel used to resolve channel frequency information + * in traffic channel broadcast messaging. + * + * @param preloadData containing a DMR network configuration monitor. + */ + @Subscribe + public void preload(NXDNChannelInfoPreloadData preloadData) + { + if(mMessageProcessor != null) + { + mMessageProcessor.preload(preloadData); + } + } + + @Override + public String getProtocolDescription() + { + return "NXDN " + mConfig.getTransmissionMode(); + } + + /** + * Sets the sample rate and configures internal decoder components. + * @param sampleRate of the channel to decode + */ + public void setSampleRate(double sampleRate) + { + float samplesPerSymbol = (float)sampleRate / mConfig.getTransmissionMode().getSymbolRate(); + + if(samplesPerSymbol <= 4.0f) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be at least " + (mConfig.getTransmissionMode().getSymbolRate() * 4) + " Hz"); + } + + int decimation = 1; + + //Identify decimation that gets us as close to 5.0 Samples Per Symbol as possible + while(samplesPerSymbol >= 10.0) + { + decimation *= 2; + samplesPerSymbol /= 2; + } + + mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation); + mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation); + float decimatedSampleRate = (float)sampleRate / decimation; + + //Set the decimated sample rate to use for PLL error reporting. + setDecimatedSampleRate(decimatedSampleRate); + + int symbolLength = 26; + float rrcAlpha = 0.2f; + float[] rrcTaps = FilterFactory.getRootRaisedCosine(samplesPerSymbol, symbolLength, rrcAlpha); + mPulseShapingFilter = new RealFIRFilter(rrcTaps); + float[] basebandTaps = getBasebandFilter(decimatedSampleRate, mConfig); + mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps); + mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps); + mDemodulator = FmDemodulatorFactory.getFmDemodulator(); + mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol); + mMessageProcessor.setMessageListener(getMessageListener()); + } + + /** + * Primary method for processing incoming complex sample buffers + * @param samples containing channelized complex samples + */ + @Override + public void receive(ComplexSamples samples) + { + //Update the message framer with the timestamp from the incoming sample buffer. + mMessageFramer.setTimestamp(samples.timestamp()); + + float[] i = mDecimationFilterI.decimateReal(samples.i()); + float[] q = mDecimationFilterQ.decimateReal(samples.q()); + + i = mBasebandFilterI.filter(i); + q = mBasebandFilterQ.filter(q); + + float[] demodulated = mDemodulator.demodulate(i, q); + demodulated = mPulseShapingFilter.filter(demodulated); + + //Process demodulated samples into symbols and apply message sync detection and framing. + mSymbolProcessor.process(demodulated); + } + + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ + private float[] getBasebandFilter(double sampleRate, DecodeConfigNXDN config) + { + + String mapKey = config.getTransmissionMode().name() + sampleRate; + + float[] coefficients = null; + + if(BASEBAND_FILTERS.containsKey(mapKey)) + { + coefficients = BASEBAND_FILTERS.get(mapKey); + } + else + { + ChannelSpecification channel = config.getChannelSpecification(); + + FIRFilterSpecification specification = FIRFilterSpecification + .lowPassBuilder() + .sampleRate(sampleRate) + .passBandCutoff(channel.getPassFrequency()) + .passBandAmplitude(1.0).passBandRipple(0.01) + .stopBandAmplitude(0.0).stopBandStart(channel.getStopFrequency()) + .stopBandRipple(0.01).build(); + + try + { + coefficients = FilterFactory.getTaps(specification); + + if(coefficients != null) + { + BASEBAND_FILTERS.put(mapKey, coefficients); + } + } + catch(Exception fde) //FilterDesignException + { + System.out.println("Error"); + } + } + + if(coefficients == null) + { + throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]"); + } + + return coefficients; + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public void setBufferListener(Listener listener) + { + mSymbolProcessor.setBufferListener(listener); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public void removeBufferListener(Listener listener) + { + mSymbolProcessor.setBufferListener(null); + } + + /** + * Implements the IByteBufferProvider interface - delegates to the symbol processor + */ + @Override + public boolean hasBufferListeners() + { + return mSymbolProcessor.hasBufferListener(); + } + + @Override + public Listener getSourceEventListener() + { + return this::process; + } + + @Override + public void stop() + { + super.stop(); + mSymbolProcessor.dispose(); + } + + /** + * Process source events + */ + private void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + mSymbolProcessor.resetBalance(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + } + } + + @Override + public Listener getComplexSamplesListener() + { + return this; + } + + public static class MyListener implements Listener + { + private int mInvalidMessages; + private int mValidMessages; + private int mValidMessageBitErrors; + + public void log() + { + System.out.println("Invalid: " + mInvalidMessages + " Valid: " + mValidMessages + " Valid-Bit Errors: " + mValidMessageBitErrors); + } + + @Override + public void receive(IMessage message) + { + if(message instanceof NXDNMessage nxdn) + { + if(nxdn.isValid()) + { + System.out.println("BE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn); + mValidMessages++; + mValidMessageBitErrors += nxdn.getMessage().getCorrectedBitCount(); + } + else + { + System.out.println("\t\tBE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn); + mInvalidMessages++; + } + } + else + { + System.out.println(message.toString()); + } + } + } + + public static void main(String[] args) + { + LOGGER.info("Starting ..."); + + DecodeConfigNXDN config = null; +// String directory = "D:\\Recordings\\NXDN\\"; //Windows + String directory = "/run/media/denny/T9/Recordings/NXDN/"; //Linux + + //NXDN 9600 Channels +// config = new DecodeConfigNXDN(TransmissionMode.M9600); +// config.add(new ChannelFrequency(105, 451887500, 0)); +// String file = directory + "20251127_063701_451887500_Bush-NXDN-96_Sentinel-Heights_Control_45_baseband.wav"; + + //Weird 938 MHz site in Springfield, VA that is all CRC errors. +// String file = directory + "20260324_033833_938736125_SYSTEM_SITE_null_47_baseband.wav"; + + //NXDN 4800 Channels + config = new DecodeConfigNXDN(TransmissionMode.M4800); + String file = directory + "20251128_052514_150845000_MobileTech-NXDN-48_Fulton_Control_47_baseband.wav"; + //Lower SNR recordings +// String file = directory + "20260430_023716_150867500_Mobiletech-Communications-(NXDN)_Pulaski_Pulaski_11_baseband.wav"; +// String file = directory + "20260430_023556_152982500_Mobiletech-Communications-(NXDN)_Manlius_Manlius_15_baseband.wav"; + + //This traffic channel sample Has Sync Detects between 1,096,430 - 1,111,430 samples (decimated sample rate: 12,500 Hz) +// String file = directory + "20260104_065056_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav"; +// String file = directory + "20260104_065240_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav"; +// String file = directory + "AceVentura Sample 1/20260122_071724_154987500_SCFR_SITE_null_1_baseband.wav"; +// String file = directory + "AceVentura Sample 2/20260122_073239_154987500_SCFR_SITE_null_1_baseband.wav"; +// String file = directory + "20260322_063443_150845000_Mobiletech-Communications-(NXDN)_Fulton_Fulton_2_baseband.wav"; + + + //Type-D Channels +// config = new DecodeConfigNXDN(TransmissionMode.TYPE_D); +// String file = directory + "Type C and D/Action Radio Hennepin tadscottsmith/20260402_152326_464125000_Action-Radio---Hennepin_LCN-1_103_baseband.wav"; +// String file = directory + "Type C and D/Action Radio Hennepin 2 tadscottsmith/recordings/20260330_221120_464650000_Action-Radio_Hennepin_LCN-4_92_baseband.wav"; +// String file = directory + "Type C and D/20260314_091952_451775000_Heimes_Omaha_TYPE_D_114_baseband.wav"; +// String file = directory + "Type C and D/20260318_034719_452675000_Heimes_Omaha_TYPE_D_0_baseband.wav"; +// String file = directory + "Type C and D/20260320_065339_451450000_Heimes_Omaha_TYPE_D_0_baseband.wav"; + + //This file has PPM mis-adjusted by 8.0 to test for equalizer balance +// String file = directory + "20260119_044740_150845000_Mobiletech-Communications-(NXDN)_Oswego_Control_2_baseband.wav"; + + System.out.println("Processing File: " + file); + boolean autoReplay = false; + + NXDNDecoder decoder = new NXDNDecoder(config); + decoder.start(); + + MyListener listener = new MyListener(); + decoder.setMessageListener(listener); + + try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay)) + { + source.setListener(iNativeBuffer -> { + Iterator it = iNativeBuffer.iterator(); + + while(it.hasNext()) + { + decoder.receive(it.next()); + } + }); + source.start(); + decoder.setSampleRate(source.getSampleRate()); + System.out.println("Recording File Sample Rate: " + source.getSampleRate()); + + while(true) + { + source.next(2048, true); + } + } + catch(IOException ioe) + { + LOGGER.error("Error", ioe); + } + + listener.log(); + + LOGGER.info("Finished"); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java new file mode 100644 index 00000000..df0acfe3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java @@ -0,0 +1,738 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent; +import io.github.dsheirer.channel.state.DecoderState; +import io.github.dsheirer.channel.state.DecoderStateEvent; +import io.github.dsheirer.channel.state.State; +import io.github.dsheirer.controller.channel.Channel; +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.MutableIdentifierCollection; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.event.DecodeEventType; +import io.github.dsheirer.module.decode.event.PlottableDecodeEvent; +import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio; +import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment; +import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignmentDuplicateControl; +import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallReceptionRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequestWithESN; +import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallRequestHeader; +import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateControl; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallConnectionResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallReceptionRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS; +import io.github.dsheirer.module.decode.nxdn.layer3.data.NXDNPacketMessage; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequest; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequestMultiSystem; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponseMultiSystem; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationParameterInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponseTypeD; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationClearResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationCommand; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponse; +import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponseTypeD; +import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasComplete; +import io.github.dsheirer.protocol.Protocol; +import java.util.Collections; +import java.util.List; + +/** + * NXDN decoder state + */ +public class NXDNDecoderState extends DecoderState +{ + private static final int IDLE_DURING_CALL_MAX_COUNT = 5; + private final Channel mChannel; + private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor(); + private final NXDNTrafficChannelManager mTrafficChannelManager; + private boolean mEncryptedCallStateDetermined = false; + private boolean mEncryptedCall = false; + private int mDisconnectResponseCount = 0; + private int mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT; + + /** + * Constructs an instance + * @param channel for this state + * @param trafficChannelManager shared with this channel + */ + public NXDNDecoderState(Channel channel, NXDNTrafficChannelManager trafficChannelManager) + { + mChannel = channel; + mTrafficChannelManager = trafficChannelManager; + } + + /** + * Indicates if we have a non-null traffic channel manager + */ + private boolean hasTrafficChannelManager() + { + return mTrafficChannelManager != null; + } + + @Override + public void init() + { + } + + @Override + public void start() + { + super.start(); + + //Change the default (45-second) traffic channel timeout to 1 second + if(mChannel.isTrafficChannel()) + { + broadcast(new ChangeChannelTimeoutEvent(this, Channel.ChannelType.TRAFFIC, 1000)); + } + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.NXDN; + } + + @Override + public void receiveDecoderStateEvent(DecoderStateEvent event) + { + switch(event.getEvent()) + { + case REQUEST_RESET: + resetState(); + break; + case NOTIFICATION_SOURCE_FREQUENCY: + setCurrentFrequency(event.getFrequency()); + + //Only update the traffic channel manager if we're not a traffic channel. + if(hasTrafficChannelManager() && mChannel.isStandardChannel()) + { + mTrafficChannelManager.setCurrentControlFrequency(getCurrentFrequency(), mChannel); + } + break; + default: + break; + } + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + sb.append("NXDN Channel Activity Summary\n"); + + if(mChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN) + { + sb.append("Transmission Mode: ").append(configNXDN.getTransmissionMode()).append("\n"); + } + sb.append(mNetworkConfigurationMonitor.getSummary()).append("\n"); + sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()).append("\n"); + return sb.toString(); + } + + @Override + public void receive(IMessage iMessage) + { + if(iMessage instanceof NXDNMessage nxdn && nxdn.isValid()) + { + if(nxdn instanceof NXDNLayer3Message layer3) + { + processLayer3(layer3); + } + else if(nxdn instanceof Audio audio) + { + processAudio(audio); + } + else if(nxdn instanceof NXDNPacketMessage packetMessage) + { + processPacketMessage(packetMessage); + } + } + } + + /** + * Broadcasts the arguments as a new decode event + * @param identifiers involved in the event + * @param timestamp of the message/event + * @param decodeEventType of the event + * @param details for the event + */ + private void broadcastEvent(List identifiers, long timestamp, DecodeEventType decodeEventType, String details) + { + MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers); + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); + broadcast(NXDNDecodeEvent.builder(decodeEventType, timestamp) + .channel(getCurrentChannel()) + .details(details) + .identifiers(mic) + .build()); + } + + /** + * Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifiers + * passing each identifier through the patch group manager to replace with a patch group if it exists + * @param identifiers to add to the collection copy + * @return collection + */ + private MutableIdentifierCollection getMutableIdentifierCollection(List identifiers) + { + MutableIdentifierCollection requestCollection = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + requestCollection.remove(IdentifierClass.USER); + + for(Identifier identifier: identifiers) + { + requestCollection.update(identifier); + } + + return requestCollection; + } + + /** + * Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifier + * passed through the patch group manager to replace with a patch group if it exists + * @param identifier to add to the collection copy + * @return collection + */ + private MutableIdentifierCollection getMutableIdentifierCollection(Identifier identifier) + { + return getMutableIdentifierCollection(Collections.singletonList(identifier)); + } + + /** + * Process layer 3 messages + * @param layer3 message to process + */ + private void processLayer3(NXDNLayer3Message layer3) + { + State state = layer3.getMessageType().isControl() ? State.CONTROL : State.ACTIVE; + DecoderStateEvent.Event event = DecoderStateEvent.Event.DECODE; + + switch(layer3.getMessageType()) + { + case CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE: + case TYPE_D_OUT_00_CC_CALL_RESPONSE: + if(layer3 instanceof VoiceCallResponse vcr) + { + //Controller responds to a voice call request with either a channel grant, or this message if + //it can't support the call or the called party is not available + broadcastEvent(vcr.getIdentifiers(), vcr.getTimestamp(), DecodeEventType.RESPONSE, + "VOICE CALL REQUEST - RESPONSE:" + vcr.getCause().toString()); + } + break; + case TRAFFIC_OUT_01_CC_VOICE_CALL: + case TYPE_D_OUT_01_CC_VOICE_CALL: + if(layer3 instanceof VoiceCall vc) + { + mIdleDuringCallCount = 0; + getIdentifierCollection().update(vc.getIdentifiers()); + mEncryptedCallStateDetermined = true; + mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted(); + state = mEncryptedCall ? State.ENCRYPTED : State.CALL; + event = DecoderStateEvent.Event.START; + mTrafficChannelManager.processVoiceCall(vc, getCurrentChannel()); + } + break; + case CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST: + case TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST: + if(layer3 instanceof VoiceCallReceptionRequest vcrr) + { + broadcastEvent(vcrr.getIdentifiers(), vcrr.getTimestamp(), DecodeEventType.PAGE, + "VOICE CALL RECEPTION REQUEST"); + } + break; + case CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE: + if(layer3 instanceof VoiceCallConnectionResponse vccr) + { + //Controller responds to a voice call connection request for the called party with either a channel + //grant, or this message if it can't support the call or connect the called party + broadcastEvent(vccr.getIdentifiers(), vccr.getTimestamp(), DecodeEventType.RESPONSE, + "CALL CONNECTION REQUEST - RESPONSE:" + vccr.getCause().toString()); + } + break; + case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR: + case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR: + state = State.CALL; + event = DecoderStateEvent.Event.CONTINUATION; + mIdleDuringCallCount = 0; + break; + case CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT: + case TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT: + case TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT: + if(layer3 instanceof VoiceCallAssignment vca) + { + //Decode event is created by the traffic channel manager + mTrafficChannelManager.processVoiceCallAssignment(vca); + } + break; + case CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE: + if(layer3 instanceof VoiceCallAssignmentDuplicateControl vcadc) + { + //This informs when there are 2-calls ongoing where a radio can participate in either call + mTrafficChannelManager.processVoiceCallAssignment(vcadc); + } + break; + case TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE: + if(layer3 instanceof VoiceCallAssignmentDuplicateTraffic vcadt) + { + //This informs when there are 2-calls ongoing where a radio can participate in either call + mTrafficChannelManager.processVoiceCallAssignment(vcadt); + } + break; + case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION: + case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE: + case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE: + mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp()); + mEncryptedCallStateDetermined = false; + mEncryptedCall = false; + mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT; + //Only remove the FROM identifier, anticipating a continuation call for the same talkgroup + getIdentifierCollection().remove(Role.FROM); + break; + case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE: + if(layer3 instanceof DataCallResponse dcr) + { + //Controller responds to a data call request with either a channel grant, or this message if + //it can't support the call or the called party is not available + broadcastEvent(dcr.getIdentifiers(), dcr.getTimestamp(), DecodeEventType.RESPONSE, + "DATA CALL REQUEST - RESPONSE:" + dcr.getCause().toString()); + } + break; + case TRAFFIC_OUT_09_CC_DATA_CALL_HEADER: + case TYPE_D_OUT_09_CC_DATA_CALL_HEADER: + state = State.DATA; + break; + case CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST: + if(layer3 instanceof DataCallReceptionRequest dcrr) + { + broadcastEvent(dcrr.getIdentifiers(), dcrr.getTimestamp(), DecodeEventType.PAGE, + "DATA CALL RECEPTION REQUEST"); + } + break; + case TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST: + break; + case TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK: + case TYPE_D_OUT_11_CC_DATA_CALL_BLOCK: + state = State.DATA; + break; + case TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE: + case TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE: + break; + case CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE: + if(layer3 instanceof DataCallAssignmentDuplicateControl dcadc) + { + mTrafficChannelManager.processDataCallAssignment(dcadc); + } + break; + case TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE: + break; + case CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT: + if(layer3 instanceof DataCallAssignment dca) + { + mTrafficChannelManager.processDataCallAssignment(dca); + } + break; + case TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT: + break; + case TRAFFIC_OUT_15_CC_HEADER_DELAY: + case TYPE_D_OUT_15_CC_HEADER_DELAY: + break; + case CONTROL_OUT_16_CC_IDLE: + break; + case TRAFFIC_OUT_16_CC_IDLE: + case TYPE_D_OUT_16_CC_IDLE: + //Hack: on both 4800 and 9600 systems there can be instances during a call where they send IDLE frames + //We'll track when we're in a call and continue a call state until the sequence of IDLE messages + //exceeds a threshold and then the state will be flipped to ACTIVE from CALL. + mIdleDuringCallCount++; + + if(mIdleDuringCallCount < IDLE_DURING_CALL_MAX_COUNT) + { + state = State.CALL; + } + else + { + mEncryptedCallStateDetermined = false; + mEncryptedCall = false; + } + break; + case CONTROL_OUT_17_CC_DISCONNECT: + //Ignore. If this happens on the current/active control channel, in a multi-channel configuration, + //the frequency rotation manager will simply move to the next channel. + break; + case TRAFFIC_OUT_17_CC_DISCONNECT: + case TYPE_D_OUT_17_CC_DISCONNECT: + mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp()); + mEncryptedCallStateDetermined = false; + mEncryptedCall = false; + getIdentifierCollection().remove(IdentifierClass.USER); + mDisconnectResponseCount++; + + //Delay channel teardown until after we receive at least 2x disconnect responses + if(mChannel.isTrafficChannel() && mDisconnectResponseCount >= 2) + { + //Signal that this traffic channel has ended. + event = DecoderStateEvent.Event.END; + state = State.FADE; + mDisconnectResponseCount = 0; + } + break; + + //Broadcast messages + case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION: + case CONTROL_OUT_24_BC_SITE_INFORMATION: + case CONTROL_OUT_25_BC_SERVICE_INFORMATION: + case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION: + case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION: + mNetworkConfigurationMonitor.process(layer3); + getIdentifierCollection().remove(IdentifierClass.USER); + break; + case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION: + mNetworkConfigurationMonitor.process(layer3); + + if(layer3 instanceof ControlChannelInformation cci) + { + setCurrentChannel(cci.getChannel1()); + } + break; + case TRAFFIC_OUT_24_BC_SITE_INFORMATION: + if(layer3 instanceof SiteInformation si) + { + //Update traffic channel manager so it can use this when allocation traffic channels. + mTrafficChannelManager.setChannelAccessInformation(si.getChannelAccessInformation()); + } + //Deliberate fall through + case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION: + case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID: + case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION: + case TYPE_D_OUT_25_BC_SERVICE_INFORMATION: + case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION: + case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION: + case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION: + case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION: + mNetworkConfigurationMonitor.process(layer3); + break; + case CONTROL_OUT_32_MM_REGISTRATION_RESPONSE: + if(layer3 instanceof RegistrationResponse rr) + { + broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString()); + } + break; + case TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE: + if(layer3 instanceof RegistrationResponseTypeD rr) + { + broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString()); + } + break; + case CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE: + case TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE: + if(layer3 instanceof RegistrationClearResponse rcr) + { + broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.RESPONSE, + "CLEAR REGISTRATION REQUEST - RESPONSE:" + rcr.getCause()); + } + break; + case CONTROL_OUT_35_MM_REGISTRATION_COMMAND: + if(layer3 instanceof RegistrationCommand rc) + { + broadcastEvent(rc.getIdentifiers(), rc.getTimestamp(), DecodeEventType.REGISTER, + rc.getRegistrationOption().isPriorityStation() ? "PRIORITY STATION" : ""); + } + break; + case CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE: + if(layer3 instanceof GroupRegistrationResponse grr) + { + broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE, + "GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause()); + } + break; + case TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE: + if(layer3 instanceof GroupRegistrationResponseTypeD grr) + { + broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE, + "GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause()); + } + break; + case TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION: + if(layer3 instanceof AuthenticationParameterInformation api) + { + broadcastEvent(api.getIdentifiers(), api.getTimestamp(), DecodeEventType.REQUEST, + "AUTHENTICATE: " + api.getAuthenticationParameter()); + } + break; + case CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST: + case TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST: + if(layer3 instanceof AuthenticationInquiryRequest air) + { + broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST, + "AUTHENTICATE:" + air.getAuthenticationParameter()); + } + break; + case TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE: + if(layer3 instanceof AuthenticationInquiryResponse air) + { + broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.RESPONSE, + "AUTHENTICATION:" + air.getAuthenticationValue()); + } + break; + case CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM: + case TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM: + if(layer3 instanceof AuthenticationInquiryRequestMultiSystem air) + { + broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST, + "AUTHENTICATE (MULTI-SYSTEM):" + air.getAuthenticationParameter()); + } + break; + case TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM: + if(layer3 instanceof AuthenticationInquiryResponseMultiSystem airms) + { + broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp()) + .channel(getCurrentChannel()) + .identifiers(getMutableIdentifierCollection(airms.getIdentifiers())) + .details("AUTHENTICATION: " + airms.getAuthenticationValue()) + .build()); + } + break; + case TYPE_D_OUT_43_MM_DATA_WRITE_HEADER: + case TYPE_D_OUT_44_MM_DATA_WRITE_DATA: + state = State.DATA; + break; + case CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST: + case TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST: + case TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST: + if(layer3 instanceof StatusInquiryRequest sir) + { + broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.REQUEST, + "STATUS INQUIRY"); + } + break; + case CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE: + case TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE: + case TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE: + if(layer3 instanceof StatusInquiryResponse sir) + { + broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.RESPONSE, + "STATUS INQUIRY:" + sir.getStatus() + ":" + sir.getStatusValue() + " CAUSE:" + sir.getCause()); + } + break; + case CONTROL_OUT_50_CC_STATUS_REQUEST: + case TRAFFIC_OUT_50_CC_STATUS_REQUEST: + case TYPE_D_OUT_50_CC_STATUS_REQUEST: + if(layer3 instanceof StatusRequest sr) + { + broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST, + "STATUS REQUEST:" + sr.getStatus()); + } + break; + case CONTROL_OUT_51_CC_STATUS_RESPONSE: + case TRAFFIC_OUT_51_CC_STATUS_RESPONSE: + case TYPE_D_OUT_51_CC_STATUS_RESPONSE: + if(layer3 instanceof StatusResponse sr) + { + broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST, + "STATUS RESPONSE:" + sr.getCause()); + } + break; + case CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST: + case TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST: + case TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST: + if(layer3 instanceof RemoteControlRequest rcr) + { + broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST, + "REMOTE CONTROL REQUEST - PARAMETERS:" + rcr.getControlParameters()); + } + break; + case CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE: + case TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE: + case TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE: + if(layer3 instanceof RemoteControlResponse rcr) + { + broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST, + "REMOTE CONTROL " + rcr.getControlCommand() + " " + rcr.getCause()); + } + break; + case CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN: + if(layer3 instanceof RemoteControlRequestWithESN rcrwe) + { + broadcastEvent(rcrwe.getIdentifiers(), rcrwe.getTimestamp(), DecodeEventType.REQUEST, + "REMOTE CONTROL REQUEST WITH ESN - PARAMETERS:" + rcrwe.getAuthenticationParameter()); + } + break; + case CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER: + case TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER: + case TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER: + if(layer3 instanceof ShortDataCallRequestHeader sdcrh) + { + broadcast(NXDNDecodeEvent.builder(DecodeEventType.REQUEST, layer3.getTimestamp()) + .channel(getCurrentChannel()) + .identifiers(getMutableIdentifierCollection(sdcrh.getIdentifiers())) + .details("SHORT DATA CALL") + .build()); + state = State.DATA; + } + break; + case TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK: + case TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA: + state = State.DATA; + case CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA: + break; + case TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR: + case TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR: + state = State.DATA; + case CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR: + break; + case CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE: + case TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE: + case TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE: + if(layer3 instanceof ShortDataCallResponse sdcr) + { + broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp()) + .channel(getCurrentChannel()) + .identifiers(getMutableIdentifierCollection(sdcr.getIdentifiers())) + .details("SHORT DATA CALL: " + sdcr.getCause()) + .build()); + } + break; + + case PROPRIETARY_FORM: + break; + case TALKER_ALIAS: + state = State.CALL; + event = DecoderStateEvent.Event.CONTINUATION; + break; + + case TALKER_ALIAS_COMPLETE: + if(layer3 instanceof TalkerAliasComplete tac) + { + state = State.CALL; + event = DecoderStateEvent.Event.CONTINUATION; + var radio = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM); + + if(radio instanceof RadioIdentifier ri) + { + mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), tac.getTalkerAlias(), ri, tac.getTimestamp()); + } + } + break; + + case TRAFFIC_IN_01_CC_VOICE_CALL: + state = State.CALL; + break; + case TRAFFIC_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE: + case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR: + state = State.CALL; + break; + case TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE: + case TRAFFIC_IN_09_CC_DATA_CALL_HEADER: + state = State.DATA; + break; + case TRAFFIC_IN_11_CC_DATA_CALL_BLOCK: + state = State.DATA; + break; + case TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE: + case TRAFFIC_IN_15_CC_HEADER_DELAY: + case TRAFFIC_IN_16_CC_IDLE: + case TRAFFIC_IN_17_CC_DISCONNECT_REQUEST: + case TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM: + case TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM: + case TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST: + case TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE: + case TRAFFIC_IN_50_CC_STATUS_REQUEST: + case TRAFFIC_IN_51_CC_STATUS_RESPONSE: + case TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST: + case TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE: + case TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER: + case TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK: + state = State.DATA; + break; + case TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR: + state = State.DATA; + break; + + case UNKNOWN: + break; + } + + broadcast(new DecoderStateEvent(this, event, state)); + } + + /** + * Process an audio message + * @param audio to process + */ + private void processAudio(Audio audio) + { + mIdleDuringCallCount = 0; + State state = State.CALL; + + if(mEncryptedCallStateDetermined && mEncryptedCall) + { + state = State.ENCRYPTED; + } + + mTrafficChannelManager.processCallProgressUpdate(getCurrentChannel(), audio.getTimestamp()); + + broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, state)); + } + + /** + * Process packet-based messages + * @param message to process + */ + private void processPacketMessage(NXDNPacketMessage message) + { + if(message instanceof GPS gps) + { + PlottableDecodeEvent plottableEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, gps.getTimestamp()) + .identifiers(new IdentifierCollection(message.getPacketSequence().getHeader().getIdentifiers())) + .channel(getCurrentChannel()) + .details(gps.getLocationFormatted() + " HDG: " + gps.getHeading() + " SPD:" + gps.getSpeed() + + " ELE:" + gps.getElevation()) + .protocol(Protocol.NXDN) + .location(gps.getLocation()) + .heading(gps.getHeading()) + .speed(gps.getSpeed()) + .build(); + broadcast(plottableEvent); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java new file mode 100644 index 00000000..37149794 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java @@ -0,0 +1,155 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.nxdn.layer2.LICH; +import io.github.dsheirer.protocol.Protocol; + +/** + * Base NXDN message implementation + */ +public abstract class NXDNMessage implements IMessage +{ + protected static int OCTET_0 = 0; + protected static int OCTET_1 = 8; + protected static int OCTET_2 = 16; + protected static int OCTET_3 = 24; + protected static int OCTET_4 = 32; + protected static int OCTET_5 = 40; + protected static int OCTET_6 = 48; + protected static int OCTET_7 = 56; + protected static int OCTET_8 = 64; + protected static int OCTET_9 = 72; + protected static int OCTET_10 = 80; + protected static int OCTET_11 = 88; + protected static int OCTET_12 = 96; + protected static int OCTET_13 = 104; + protected static int OCTET_14 = 112; + protected static int OCTET_15 = 120; + protected static int OCTET_16 = 128; + protected static int OCTET_17 = 136; + protected static int OCTET_18 = 144; + protected static int OCTET_19 = 152; + protected static int OCTET_20 = 160; + protected final int mRAN; + protected final LICH mLICH; + + private CorrectedBinaryMessage mMessage; + private long mTimestamp; + private boolean mValid = true; + + /** + * Constructs an instance + * + * @param message with binary data + * @param timestamp for the message + */ + public NXDNMessage(CorrectedBinaryMessage message, long timestamp, int ran, LICH lich) + { + mMessage = message; + mTimestamp = timestamp; + mRAN = ran; + mLICH = lich; + } + + @Override + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Underlying binary message + */ + public CorrectedBinaryMessage getMessage() + { + return mMessage; + } + + @Override + public boolean isValid() + { + return mValid; + } + + /** + * Marks this message as valid or invalid. + * + * @param valid flag + */ + public void setValid(boolean valid) + { + mValid = valid; + } + + @Override + public Protocol getProtocol() + { + return Protocol.NXDN; + } + + @Override + public int getTimeslot() + { + return 0; + } + + /** + * Formatted message prefix indicating CRC status, RAN and RF Channel. + */ + public StringBuilder getMessageBuilder() + { + StringBuilder sb = new StringBuilder(); + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + sb.append(getLICH().getRFChannel().name()); + if(hasRAN()) + { + sb.append(" RAN:").append(getRAN()); + } + sb.append(" "); + return sb; + } + + public boolean hasRAN() + { + return mRAN != 0; + } + + /** + * Radio access network (RAN) from the frame carrier + */ + public int getRAN() + { + return mRAN; + } + + /** + * Link Information Channel (LICH) from the frame carrier + */ + public LICH getLICH() + { + return mLICH; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java new file mode 100644 index 00000000..456f3ff1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java @@ -0,0 +1,251 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.SyncLossMessage; +import io.github.dsheirer.module.decode.nxdn.layer1.Frame; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNHardSyncDetector; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector; +import io.github.dsheirer.module.decode.nxdn.layer2.LICHTracker; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +import io.github.dsheirer.protocol.Protocol; +import io.github.dsheirer.sample.Listener; +import java.util.Arrays; + +/** + * Provides message framing for the demodulated dibit stream. This framer is notified by an external sync detection + * process using the two syncDetected() methods below to indicate if the NID that follows the sync was correctly error + * detected and corrected. When the NID does not pass error correction, we use a PLACEHOLDER data unit ID to allow the + * uncertain message to assemble and then we'll inspect before and after data unit IDs and the quantity of captured + * dibits to make a best guess on what the assembled message represents. + */ +public class NXDNMessageFramer +{ + private static final int FRAME_LENGTH = 192; + private static final int SYNC_LENGTH = 10; + private static final int PAYLOAD_LENGTH = FRAME_LENGTH - SYNC_LENGTH; + private static final int SYNC_DETECTION_BIT_ERROR_THRESHOLD = 8; + private final Dibit[] mDibitBuffer = new Dibit[192]; + private final LICHTracker mLICHTracker; + private final Listener mMessageListener; + private final NXDNHardSyncDetector mHardSyncDetector = new NXDNHardSyncDetector(); + private final int mSymbolsPerSecond; + private final int mCoarseSyncLossThreshold; + private boolean mSyncDetected = false; + private int mDibitBufferPointer = 0; + private int mDibitSinceTimestampCounter = 0; + private int mDibitsSinceLastMessage = FRAME_LENGTH; + private long mReferenceTimestamp = 0; + + /** + * Constructs an instance + * @param messageListener to receive framed messages + * @param transmissionMode to indicate how many symbols per second for accounting + */ + public NXDNMessageFramer(Listener messageListener, TransmissionMode transmissionMode) + { + mMessageListener = messageListener; + mLICHTracker = new LICHTracker(transmissionMode); + mSymbolsPerSecond = transmissionMode.getSymbolRate(); + mCoarseSyncLossThreshold = mSymbolsPerSecond + FRAME_LENGTH; + Arrays.fill(mDibitBuffer, Dibit.D00_PLUS_1); + } + + /** + * Sets or updates the current dibit stream time from an incoming sample buffer. + * @param time to use as a reference timestamp. + */ + public void setTimestamp(long time) + { + mReferenceTimestamp = time; + mDibitSinceTimestampCounter = 0; + } + + + /** + * Calculates the timestamp accurate to the currently received dibit. + * @return timestamp in milliseconds. + */ + private long getTimestamp() + { + if(mReferenceTimestamp > 0) + { + return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / 4800); + } + else + { + mDibitSinceTimestampCounter = 0; + return System.currentTimeMillis(); + } + } + + /** + * Dispatches a currently assembling message once it's complete. + * @return true if any of the messages from the frame are valid. + */ + private boolean dispatchMessage() + { + CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(PAYLOAD_LENGTH * 2); + //The buffer pointer is the next symbol after the completed message, ie start of sync + int bufferDibitPointer = mDibitBufferPointer + SYNC_LENGTH; + bufferDibitPointer %= FRAME_LENGTH; + int messageDibitPointer = 0; + + Dibit dibit; + while(messageDibitPointer < PAYLOAD_LENGTH) + { + dibit = mDibitBuffer[bufferDibitPointer++]; + cbm.add(dibit.getBit1(), dibit.getBit2()); + bufferDibitPointer %= FRAME_LENGTH; + messageDibitPointer++; + } + + boolean validMessage = false; + Frame frame = new Frame(cbm, getTimestamp(), mLICHTracker); + + for(NXDNMessage message: frame.getMessages()) + { + validMessage |= message.isValid(); + dispatch(message); + } + + mDibitsSinceLastMessage = 0; + + return validMessage; + } + + /** + * Tests if the previous 10 symbols in the dibit buffer match the sync pattern. This uses the + * buffer wrapping to determine if the just collected dibit sync sequence contains a sync pattern but was not + * triggered by the sync detector. This should only be used at the message interval when we anticipate arrival of + * the succeeding message. + * + * @return bit errors for the received dibit sequence against the sync pattern + */ + private int checkSync() + { + long value = 0; + int dibitBufferPointer = mDibitBufferPointer - SYNC_LENGTH; + + if(dibitBufferPointer < 0) + { + dibitBufferPointer += FRAME_LENGTH; + } + + for(int x = 0; x < SYNC_LENGTH; x++) + { + value <<= 2; + value += mDibitBuffer[dibitBufferPointer++].getValue(); + dibitBufferPointer %= FRAME_LENGTH; + } + + return Long.bitCount(value ^ NXDNSyncDetector.SYNC_PATTERN); + } + + /** + * Dispatches the message to an optional listener + * @param message to dispatch + */ + private void dispatch(IMessage message) + { + if(mMessageListener != null) + { + mMessageListener.receive(message); + } + } + + /** + * Notification that the sync pattern was detected. The most recently received dibit should be the last symbol + * in the sync pattern and the next dibit received should be the start of the frame content. + */ + public void syncDetected() + { + //Ignore false/extra sync when we're already in sync detection + if(!mSyncDetected) + { + mSyncDetected = true; + + //Update sync loss accounting when we have more/less than the expected sync pattern symbol count. + if(mDibitsSinceLastMessage > SYNC_LENGTH) + { + dispatch(new SyncLossMessage(getTimestamp(), + (mDibitsSinceLastMessage - SYNC_LENGTH) * 2, Protocol.NXDN)); + mDibitsSinceLastMessage = SYNC_LENGTH; + } + else if(mDibitsSinceLastMessage < SYNC_LENGTH) + { + mDibitsSinceLastMessage = SYNC_LENGTH; + } + } + } + + /** + * Process symbol decision without sync detection. + * @param symbol that was demodulated. + */ + public boolean process(Dibit symbol) + { + //Check when we didn't get a sync detect, and count-wise, we're at the next frame's end-sync location + if(!mSyncDetected && mDibitsSinceLastMessage == SYNC_LENGTH) + { + int bitErrors = checkSync(); + + if(bitErrors <= SYNC_DETECTION_BIT_ERROR_THRESHOLD) + { + mSyncDetected = true; + } + } + + mDibitBuffer[mDibitBufferPointer++] = symbol; + mDibitBufferPointer %= FRAME_LENGTH; + mDibitsSinceLastMessage++; + mDibitSinceTimestampCounter++; + + if(mSyncDetected && mDibitsSinceLastMessage == FRAME_LENGTH) + { + mSyncDetected = false; + return dispatchMessage(); + } + else if(mDibitsSinceLastMessage >= mCoarseSyncLossThreshold) + { + mDibitsSinceLastMessage -= mSymbolsPerSecond; + dispatch(new SyncLossMessage(getTimestamp(), mSymbolsPerSecond * 2, Protocol.NXDN)); + } + + return false; + } + + /** + * Process symbol decision with (hard) sync detection + * @param symbol to process + */ + public void processWithHardSyncDetection(Dibit symbol) + { + process(symbol); + + if(mHardSyncDetector.process(symbol)) + { + syncDetected(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java new file mode 100644 index 00000000..d7038107 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java @@ -0,0 +1,206 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation; +import io.github.dsheirer.module.decode.nxdn.layer2.SACCHAssembler; +import io.github.dsheirer.module.decode.nxdn.layer2.SACCHFragment; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Idle; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect; +import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader; +import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataInitializationVector; +import io.github.dsheirer.module.decode.nxdn.layer3.call.UserData; +import io.github.dsheirer.module.decode.nxdn.layer3.data.PacketSequenceAssembler; +import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAlias; +import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasAssembler; +import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation; +import io.github.dsheirer.sample.Listener; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NXDN Message Processor. + * + * Performs post-message creation processing and enrichment before the message is sent downstream. + */ +public class NXDNMessageProcessor implements Listener +{ + private final static Logger LOGGER = LoggerFactory.getLogger(NXDNMessageProcessor.class); + private ChannelAccessInformation mChannelAccessInformation; + private final Map mChannelFrequencyMap = new HashMap<>(); + private final PacketSequenceAssembler mPacketSequenceAssembler = new PacketSequenceAssembler(); + private final SACCHAssembler mSACCHAssembler = new SACCHAssembler(); + private final TalkerAliasAssembler mTalkerAliasAssembler; + + /** + * Sets channel preload information. This is used to preload a traffic channel with system channel information + * from the control channel when a traffic channel is allocated, on startup. + * @param data to preload + */ + public void preload(NXDNChannelInfoPreloadData data) + { + ChannelInformation channelInformation = data.getData(); + if(mChannelAccessInformation == null) + { + mChannelAccessInformation = channelInformation.getChannelAccessInformation(); + } + + for(ChannelFrequency channelFrequency: channelInformation.getChannelFrequencies()) + { + if(!mChannelFrequencyMap.containsKey(channelFrequency.getChannel())) + { + mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency); + } + } + } + + /** + * Downstream message listener + */ + private Listener mMessageListener; + + /** + * Constructs an instance + * @param configNXDN that optionally contains a channel map for channel mode systems + */ + public NXDNMessageProcessor(DecodeConfigNXDN configNXDN) + { + for(ChannelFrequency channelFrequency: configNXDN.getChannelMap()) + { + mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency); + } + + mTalkerAliasAssembler = new TalkerAliasAssembler(configNXDN.getEncoding()); + } + + /** + * Processes the message for enrichment or reassembly of fragments and sends the enriched message and any additional + * messages that were created during the enrichment to the registered message listener. + * @param message to process + */ + @Override + public void receive(IMessage message) + { + List additionalMessagesToDispatch = null; + + if(message != null && message.isValid()) + { + //Capture channel access information from Site Information message + if(message instanceof SiteInformation siteInformation) + { + mChannelAccessInformation = siteInformation.getChannelAccessInformation(); + } + + //Enrich messages that carry channel information with the channel access info. + if(mChannelAccessInformation != null && message instanceof IChannelInformationReceiver receiver) + { + receiver.receive(mChannelAccessInformation, mChannelFrequencyMap); + } + + //SACCH fragments are either standalone 4/4, or a sequence of 4 in a superframe. Reassemble them. + if(message instanceof SACCHFragment fragment) + { + dispatch(message); + message = null; + //Publish the completed SACCH message to this method so that it can be processed correctly and dispatch + //the original fragment and nullify the message to ensure correct sequencing where fragment 4 is + //dispatched first, and then the assembled SACCH message. + receive(mSACCHAssembler.process(fragment)); + } + + if(message instanceof TalkerAlias fragment) + { + dispatch(message); + //Assign the completed alias to the message to dispatch below, to ensure proper message output sequence + message = mTalkerAliasAssembler.process(fragment); + } + else if(message instanceof Idle || message instanceof Disconnect) + { + //Clear any captured talker alias fragments at the end of the call or idle mode + mTalkerAliasAssembler.reset(); + } + else if(message instanceof IPacketHeader header) + { + mPacketSequenceAssembler.process(header); + } + else if(message instanceof UserData userDataBlock) + { + additionalMessagesToDispatch = mPacketSequenceAssembler.process(userDataBlock); + } + else if(message instanceof ShortDataInitializationVector iv) + { + mPacketSequenceAssembler.process(iv); + } + } + + dispatch(message); + + if(additionalMessagesToDispatch != null) + { + for(NXDNMessage additional: additionalMessagesToDispatch) + { + dispatch(additional); + } + } + } + + /** + * Post-process the message for frequency band details. + * @param message to post process and dispatch + */ + private void dispatch(IMessage message) + { + if(message != null && mMessageListener != null) + { + mMessageListener.receive(message); + } + } + + /** + * Prepares for disposal of this instance. + */ + public void dispose() + { + mMessageListener = null; + } + + /** + * Sets the message listener to receive the output from this processor. + * @param listener to receive output messages + */ + public void setMessageListener(Listener listener) + { + mMessageListener = listener; + } + + /** + * Clears a registered message listener. + */ + public void removeMessageListener() + { + mMessageListener = null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java new file mode 100644 index 00000000..aa38f1c7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java @@ -0,0 +1,249 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformationTypeD; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.DigitalStationIDInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.FailureStatusInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Neighbor; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ServiceInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterIdle; +import io.github.dsheirer.module.decode.nxdn.layer3.scch.SiteID; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Processes NXDN Layer 3 messages to assemble a snapshot of the site's broadcast configuration details + */ +public class NXDNNetworkConfigurationMonitor +{ + private ControlChannelInformation mControlChannelInformation; + private DigitalStationIDInformation mDigitalStationIDInformation; + private FailureStatusInformation mFailureStatusInformation; + private ServiceInformation mServiceInformation; + private SiteInformation mSiteInformation; + private Map mNeighborMap = new HashMap<>(); + + private AdjacentSiteInformationTypeD mTypeDNeighborA; + private AdjacentSiteInformationTypeD mTypeDNeighborB; + private Integer mTypeDRepeater; + private List mTypeDObservedRepeaters = new ArrayList<>(); + private SiteID mTypeDSiteID; + + /** + * Constructs an instance + */ + public NXDNNetworkConfigurationMonitor() + { + } + + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + if(mDigitalStationIDInformation != null) + { + sb.append("Current Channel Station ID\n ").append(mDigitalStationIDInformation).append("\n"); + } + if(mSiteInformation != null) + { + sb.append("\nCurrent Site\n ").append(mSiteInformation).append("\n"); + } + else if(mTypeDSiteID != null) + { + sb.append("\nCurrent Site\n ").append(mTypeDSiteID).append("\n"); + } + if(mServiceInformation != null) + { + sb.append("\nCurrent Site Services\n ").append(mServiceInformation).append("\n"); + } + if(mFailureStatusInformation != null) + { + sb.append("\nFailure Status\n ").append(mFailureStatusInformation).append("\n"); + } + if(mControlChannelInformation != null) + { + sb.append("\nCurrent Site Control Channel\n ").append(mControlChannelInformation).append("\n"); + } + + if(mTypeDRepeater != null) + { + sb.append("\nType-D Current Repeater: ").append(mTypeDRepeater).append("\n"); + } + + if(!mTypeDObservedRepeaters.isEmpty()) + { + Collections.sort(mTypeDObservedRepeaters); + sb.append("\nType-D Observed Repeater Numbers:").append(mTypeDObservedRepeaters).append("\n"); + } + + if(!mNeighborMap.isEmpty()) + { + sb.append("\nNeighbor Sites\n"); + List ids = new ArrayList<>(mNeighborMap.keySet()); + Collections.sort(ids); + + for(Integer id : ids) + { + sb.append(" ").append(mNeighborMap.get(id)).append("\n"); + } + } + else if(mTypeDNeighborA != null || mTypeDNeighborB != null) + { + sb.append("\nNeighbor Sites\n"); + + if(mTypeDNeighborA != null) + { + sb.append(" ").append(mTypeDNeighborA.getSystemID1()).append(" SITE:").append(mTypeDNeighborA.getSite1()).append("\n"); + + if(mTypeDNeighborA.hasSite2()) + { + sb.append(" ").append(mTypeDNeighborA.getSystemID2()).append(" SITE:").append(mTypeDNeighborA.getSite2()).append("\n"); + } + } + if(mTypeDNeighborB != null) + { + sb.append(" ").append(mTypeDNeighborB.getSystemID1()).append(" SITE:").append(mTypeDNeighborB.getSite1()).append("\n"); + + if(mTypeDNeighborB.hasSite2()) + { + sb.append(" ").append(mTypeDNeighborB.getSystemID2()).append(" SITE:").append(mTypeDNeighborB.getSite2()).append("\n"); + } + } + } + else + { + sb.append("\nNeighbor Sites\n NONE\n"); + } + + return sb.toString(); + } + + public void process(NXDNLayer3Message layer3) + { + switch(layer3.getMessageType()) + { + case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION: + case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION: + case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID: + if(layer3 instanceof DigitalStationIDInformation dsii) + { + mDigitalStationIDInformation = dsii; + } + break; + case CONTROL_OUT_24_BC_SITE_INFORMATION: + case TRAFFIC_OUT_24_BC_SITE_INFORMATION: + mSiteInformation = (SiteInformation) layer3; + break; + case CONTROL_OUT_25_BC_SERVICE_INFORMATION: + case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION: + case TYPE_D_OUT_25_BC_SERVICE_INFORMATION: + if(layer3 instanceof ServiceInformation sii) + { + mServiceInformation = sii; + } + break; + case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION: + case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION: + mControlChannelInformation = (ControlChannelInformation) layer3; + break; + case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION: + case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION: + mFailureStatusInformation = (FailureStatusInformation) layer3; + break; + case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION: + case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION: + if(layer3 instanceof AdjacentSiteInformation adjacent) + { + if(adjacent.hasChannel1()) + { + Neighbor n1 = adjacent.getNeighbor1(); + + if(n1 != null) + { + mNeighborMap.put(n1.id(), n1); + + if(adjacent.hasChannel2()) + { + Neighbor n2 = adjacent.getNeighbor2(); + + if(n2 != null) + { + mNeighborMap.put(n2.id(), n2); + + if(adjacent.hasChannel3()) + { + Neighbor n3 = adjacent.getNeighbor3(); + + if(n3 != null) + { + mNeighborMap.put(n3.id(), n3); + + if(adjacent.hasChannel4()) + { + Neighbor n4 = adjacent.getNeighbor4(); + + if(n4 != null) + { + mNeighborMap.put(n4.id(), n4); + } + } + } + } + } + } + } + } + } + case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION: + if(layer3 instanceof AdjacentSiteInformationTypeD atd) + { + if(atd.isIndex()) + { + mTypeDNeighborB = atd; + } + else + { + mTypeDNeighborA = atd; + } + } + break; + case TYPE_D_SCCH_OUT_INFO_4_REPEATER_IDLE: + if(layer3 instanceof RepeaterIdle ri) + { + mTypeDRepeater = ri.getRepeater(); + + if(!mTypeDObservedRepeaters.contains(ri.getRepeater2())) + { + mTypeDObservedRepeaters.add(ri.getRepeater2()); + } + } + break; + case TYPE_D_SCCH_OUT_INFO_4_SITE_ID: + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java new file mode 100644 index 00000000..c008d6e2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java @@ -0,0 +1,1123 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; +import io.github.dsheirer.gui.viewer.sync.FMSyncResultsViewer; +import io.github.dsheirer.gui.viewer.sync.ISyncResultsListener; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.nxdn.layer1.C4FMEqualizer; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSoftSyncDetector; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetectorFactory; +import io.github.dsheirer.module.decode.nxdn.layer1.sync.ReferenceSyncWaveform; +import io.github.dsheirer.sample.Listener; +import java.nio.ByteBuffer; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +/** + * NXDN symbol processor provides symbol timing recovery and sync detection. + */ +public class NXDNSymbolProcessor +{ + private static final float TIMING_LOOP_GAIN = 0.4f; + private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0); + private static final float SYNC_THRESHOLD_DETECTION = 37; + private static final float SYNC_THRESHOLD_OPTIMIZED = 40; + private static final float SYNC_THRESHOLD_EQUALIZED = 40; + private static final float TWO_PI = (float)(Math.PI * 2.0); + private static final int BUFFER_WORKSPACE_LENGTH = 1024; + private static final int DIBIT_LENGTH_NID = 33; //32 dibits (64 bits) +1 status + private static final int DIBIT_LENGTH_SYNC = 10; + private static final Correction INVALID_SYNC_DETECTION = new Correction(0d, Double.MAX_VALUE, 0f, 0f, 0f, 0f, 0); + private ISyncResultsListener mSyncResultsViewer; + private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300); + private final FeedbackDecoder mFeedbackDecoder; + private final NXDNMessageFramer mMessageFramer; + private final NXDNSoftSyncDetector mSyncDetector = NXDNSyncDetectorFactory.getStandardDetector(); + private final NXDNSoftSyncDetector mSyncDetectorLagging = NXDNSyncDetectorFactory.getStandardDetector(); + private final SampleEqualizer mSampleEqualizer = new SampleEqualizer(); + private final C4FMEqualizer mSymbolEqualizer; + private final SyncTracker mSyncTracker = new SyncTracker(2400, 3); + private double mNoiseStandardDeviationThreshold; + private double mSamplePoint; + private double mSamplePointAdjustment; + private double mSamplePointAdjustmentIncrement; + private double mSamplePointAdjustmentMax; + private double mSamplesPerSymbol; + private double mTimingCorrectionThreshold; + private float[] mBuffer; + private float mLaggingSyncOffset; + private float mOptimizeFineIncrement; + private int mBufferPointer; + private int mBufferReloadThreshold; + private int mSymbolsSinceLastSync = 0; + private int mDebugSymbolCounter = 0; + private int mDebugSampleCounter = 0; + private boolean mSynchronized = false; + private boolean mDelayedSyncNotificationPending = false; + private int mDelayedSyncNotificationSymbolsRemaining = 0; + private ReferenceSyncWaveform mReferenceSyncWaveform; + private final boolean mVisualize = false; + private final int mDebugSymbolStart = 200; + private static final DecimalFormat DF = new DecimalFormat("00.000"); + + + /** + * Constructs an instance + * @param messageFramer to receive symbol decisions (dibits) and sync notifications. + */ + public NXDNSymbolProcessor(NXDNMessageFramer messageFramer, FeedbackDecoder feedbackDecoder) + { + mMessageFramer = messageFramer; + mFeedbackDecoder = feedbackDecoder; + mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getSyncSymbols(), 9, 192 * 3); + DF.setPositivePrefix(" "); + } + + public void resetEqualizer() + { + mSymbolEqualizer.reset(); + } + + public String getEqualizerSettings() + { + return mSymbolEqualizer.getTapsDescription(); + } + + + public void log() + { + System.out.println("SAMPLES: " + mDebugSampleCounter + " SYMBOLS: " + mDebugSymbolCounter); + } + + /** + * Finalize and prepare for shutdown. + */ + public void dispose() + { + mDibitAssembler.flush(); + } + + /** + * Resets the balance + */ + public void resetBalance() + { + mSampleEqualizer.reset(); + } + + /** + * Primary input method for receiving a stream of demodulated samples to process into symbols. + * @param samples to process + */ + public void process(float[] samples) + { + //Create shadow copy of heap variables on stack + double samplePoint = mSamplePoint; + double samplesPerSymbol = mSamplesPerSymbol; + int bufferPointer = mBufferPointer; + int bufferReloadThreshold = mBufferReloadThreshold; + int symbolsSinceLastSync = mSymbolsSinceLastSync; + + int samplesPointer = 0; + float softSymbol, scorePrimary; + Correction correctionCandidate; + + if(!mSynchronized && samples.length > 400) + { + StandardDeviation sd = new StandardDeviation(); + + float accumulator = 0.0f; + + for(int i = 0; i < 400; i++) + { + accumulator += samples[i]; + sd.increment(samples[i]); + } + + //Only apply a balance correction when we have signal. Noise = .45-.55, Signal = 0.00 - 0.20 + if(sd.getResult() < 0.3) + { + accumulator /= 400; + accumulator += mSampleEqualizer.mBalance; + mSampleEqualizer.mBalance -= (accumulator * .3f); + } + } + + while(samplesPointer < samples.length) + { + //Note: buffer pointer can become greater than reload threshold during timing optimization at sync detect + if(bufferPointer >= bufferReloadThreshold) + { + //Do reload + int copyLength = Math.min(BUFFER_WORKSPACE_LENGTH, samples.length - samplesPointer); + System.arraycopy(mBuffer, copyLength, mBuffer, 0, mBuffer.length - copyLength); + System.arraycopy(samples, samplesPointer, mBuffer, mBuffer.length - copyLength, copyLength); + samplesPointer += copyLength; + bufferPointer -= copyLength; + } + + while(bufferPointer < bufferReloadThreshold) + { + bufferPointer++; + samplePoint--; + + mDebugSampleCounter++; + + if(samplePoint < 1) + { + mDebugSymbolCounter++; + + if(mDebugSymbolCounter % 10 == 0) + { + visualizeSymbols(bufferPointer, samplePoint); + } + + symbolsSinceLastSync++; + + //Clear synchronized state when we miss a sync period. + if(mSynchronized && symbolsSinceLastSync > 192) + { + mSampleEqualizer.reset(); + mSymbolEqualizer.syncLost(); + mSynchronized = false; + } + + softSymbol = mSampleEqualizer.getEqualizedSymbol(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], samplePoint); + + //Check for lagging sync pattern + float lag = (float)(bufferPointer + samplePoint - mLaggingSyncOffset); + int lagIntegral = (int)Math.floor(lag); + float softSymbolLag = mSampleEqualizer.getEqualizedSymbol(mBuffer[lagIntegral], mBuffer[lagIntegral + 1], lag - lagIntegral); + float scoreLag = mSyncDetectorLagging.process(softSymbolLag); + scorePrimary = mSyncDetector.process(softSymbol); + + correctionCandidate = INVALID_SYNC_DETECTION; + + if(scorePrimary > SYNC_THRESHOLD_DETECTION && scorePrimary > scoreLag) + { + correctionCandidate = mSampleEqualizer.optimize(mSyncDetector, 0, scorePrimary, + (bufferPointer + samplePoint)); + } + else if(scoreLag > SYNC_THRESHOLD_DETECTION) + { + correctionCandidate = mSampleEqualizer.optimize(mSyncDetectorLagging, -mLaggingSyncOffset, + scoreLag, (bufferPointer + samplePoint)); + } + + softSymbol = mSymbolEqualizer.process(softSymbolLag, softSymbol); + + //Broadcast the decoded soft symbol to an optionally registered listener (ie Symbol view in channel panel). + Dibit symbol = toSymbol(softSymbol); + + //Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized + mFeedbackDecoder.broadcast(softSymbol); + + if(mVisualize) + { + getSyncResultsViewer().symbol(softSymbol); + } + + //When the message framer emits at least one valid message (ie CRC check passes), send the current + // sample equalizer's balance value as a pseudo-PLL error measurement. + if(mMessageFramer.process(symbol)) + { + mFeedbackDecoder.processPLLError(mSampleEqualizer.balance()); + } + + mDibitAssembler.receive(symbol); + + //Do a final validation against the reference sync waveform + if(correctionCandidate.isValid()) + { + correctionCandidate = validateSyncWaveform(correctionCandidate, bufferPointer, samplePoint); + } + + if(correctionCandidate.isValid()) + { + mSampleEqualizer.apply(correctionCandidate); + + if(mSynchronized) + { + double adjustment = Math.clamp(correctionCandidate.getTimingCorrection(), + -mTimingCorrectionThreshold, mTimingCorrectionThreshold); + samplePoint += adjustment; + } + else + { + samplePoint += correctionCandidate.getTimingCorrection(); + } + + mDelayedSyncNotificationPending = true; + mDelayedSyncNotificationSymbolsRemaining = mSymbolEqualizer.getDelaySymbolCount(); + mSynchronized = true; + symbolsSinceLastSync = 0; + + //If the equalizer was disabled, it has bad samples contaminating the training buffer and we + // need to prime that buffer with aligned samples prior to sync detection training. + if(!mSymbolEqualizer.isEnabled()) + { + int symbols = mSyncDetector.getSyncSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount(); + double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset; + + float mid, sym; + for(int s = 0; s < symbols; s++) + { + //Mid symbol + + mid = mSampleEqualizer.getEqualizedSymbol(offset); + offset += mLaggingSyncOffset; + sym = mSampleEqualizer.getEqualizedSymbol(offset); + offset += mLaggingSyncOffset; + mSymbolEqualizer.process(mid, sym); + } + } + + mSymbolEqualizer.syncDetected(); + mSymbolEqualizer.enable(); + visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint); + } + else if(mDelayedSyncNotificationPending) + { + mDelayedSyncNotificationSymbolsRemaining--; + + if(mDelayedSyncNotificationSymbolsRemaining <= 0) + { + mMessageFramer.syncDetected(); + mDelayedSyncNotificationPending = false; + } + } + + //Add another symbol's worth of samples to the counter for processing + samplePoint += samplesPerSymbol; + } + } + } + + //Copy shadow variables back to the heap + mBufferPointer = bufferPointer; + mSamplePoint = samplePoint; + mSymbolsSinceLastSync = symbolsSinceLastSync; + } + + /** + * Validates the sync detection and candidate correction settings against a reference instance of the sync detection + * waveform. If the detected sync correlates well against the reference sync then the correction is valid. + * Otherwise, this method returns the static invalid sync correction to signal that the sync detection was invalid. + * @param correction to evaluate + * @param bufferPointer for the final (optimized) symbol in the buffer + * @param samplePoint offset from the buffer pointer to the final symbol + * @return the original correction or the static invalid sync correction + */ + private Correction validateSyncWaveform(Correction correction, int bufferPointer, double samplePoint) + { + float gain = mSampleEqualizer.mGain + correction.getGainAdjustment(); + float balance = mSampleEqualizer.mBalance + correction.getBalanceAdjustment(); + double offset = bufferPointer + samplePoint + correction.getTimingCorrection() - mReferenceSyncWaveform.samples().length; + int integral = (int)Math.ceil(offset); + double mu = integral - offset; + double accumulator = 0.0; + int index; + + //Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample + for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--) + { + index = integral + x; + float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu); + sample += balance; + sample *= gain; + sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample()); + accumulator += mReferenceSyncWaveform.samples()[x] * sample; + } + + double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score + + //Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static + // invalid sync detection to nullify the correction. + if(accumulator > threshold) + { + return correction; + } + + return INVALID_SYNC_DETECTION; + } + + /** + * Registers the listener to receive demodulated bit stream buffers. + * @param listener to register + */ + public void setBufferListener(Listener listener) + { + mDibitAssembler.setBufferListener(listener); + } + + /** + * Indicates if there is a registered buffer listener + */ + public boolean hasBufferListener() + { + return mDibitAssembler.hasBufferListeners(); + } + + /** + * Sets or updates the samples per symbol + * @param samplesPerSymbol to apply. + */ + public void setSamplesPerSymbol(float samplesPerSymbol) + { + mSamplesPerSymbol = samplesPerSymbol; + mTimingCorrectionThreshold = samplesPerSymbol / 30; + mNoiseStandardDeviationThreshold = .2; + mSamplePoint = samplesPerSymbol; + mSamplePointAdjustmentMax = samplesPerSymbol / 2; + mSamplePointAdjustmentIncrement = samplesPerSymbol / 100.0; + mOptimizeFineIncrement = samplesPerSymbol / 200f; + mLaggingSyncOffset = samplesPerSymbol / 2; + int bufferLength = BUFFER_WORKSPACE_LENGTH + (int)(Math.ceil((DIBIT_LENGTH_SYNC + DIBIT_LENGTH_NID + 2) * samplesPerSymbol)); + mBuffer = new float[bufferLength]; + mBufferReloadThreshold = mBuffer.length - (int)Math.ceil(samplesPerSymbol * (DIBIT_LENGTH_NID + 1)); + mBufferPointer = mBufferReloadThreshold; + mSampleEqualizer.configure(samplesPerSymbol); + mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol); + } + + /** + * Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol. + * @param sample in radians. + * @return symbol decision. + */ + public static Dibit toSymbol(float sample) + { + if(sample > 0) + { + return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1; + } + else + { + return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1; + } + } + + /** + * Debug viewer UI for visualizing sync detections in the sample stream/waveform. + */ + private ISyncResultsListener getSyncResultsViewer() + { + if(mSyncResultsViewer == null) + { + mSyncResultsViewer = new FMSyncResultsViewer(); + } + + return mSyncResultsViewer; + } + + /** + * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing. + * @param score from the sync detector + * @param primary sync detector (true) or secondary (false) + */ + public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) + { + if(!mVisualize) + { + return; + } + +// //Correct for the 1-symbol delay from the symbol equalizer +// samplePoint -= mSamplesPerSymbol; + int integral = (int)Math.floor(samplePoint); + samplePoint -= integral; + bufferPointer += integral; + + int offset = 3; + int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); + int end = bufferPointer + offset; + int start = end - length; + float[] symbols = new float[10]; + float[] samples = Arrays.copyOfRange(mBuffer, start, end); + for(int i = 0; i < samples.length; i++) + { + samples[i] = mSampleEqualizer.equalize(samples[i]); + } + + double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); + int symbolIntegral = (int)Math.floor(symbolPointer); + double mu = symbolPointer - symbolIntegral; + + for(int x = 0; x < 10; x++) + { + symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); + symbolPointer += mSamplesPerSymbol; + symbolIntegral = (int)Math.floor(symbolPointer); + mu = symbolPointer - symbolIntegral; + } + + float[] syncIntervals = new float[10]; + int adjust = bufferPointer - length + offset; + double pointer = bufferPointer + samplePoint - adjust; + + for(int x = 9; x >= 0; x--) + { + syncIntervals[x] = (float)pointer; + pointer -= mSamplesPerSymbol; + } + + CountDownLatch countDownLatch = new CountDownLatch(1); + getSyncResultsViewer().receive(symbols, mSyncDetector.getSyncSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, + tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") + + " EQ-B:" + mSampleEqualizer.mBalance + " EQ-G:" + mSampleEqualizer.mGain + " " + + " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), + countDownLatch); + + try + { + countDownLatch.await(); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } + + /** + * Debug method to visualize the symbol decisions + */ + public void visualizeSymbols(int bufferPointer, double samplePoint) + { + if(!mVisualize) + { + return; + } + + if(mDebugSymbolCounter < mDebugSymbolStart) + { + return; + } + + int offset = 3; + int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); + int end = bufferPointer + offset; + int start = end - length; + float[] symbols = new float[10]; + float[] decisions = new float[10]; + float[] samples = Arrays.copyOfRange(mBuffer, start, end); + for(int i = 0; i < samples.length; i++) + { + samples[i] = mSampleEqualizer.equalize(samples[i]); + } + + double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); + int symbolIntegral = (int)Math.floor(symbolPointer); + double mu = symbolPointer - symbolIntegral; + + for(int x = 0; x < 10; x++) + { + symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); + decisions[x] = toSymbol(symbols[x]).getIdealPhase(); + symbolPointer += mSamplesPerSymbol; + symbolIntegral = (int)Math.floor(symbolPointer); + mu = symbolPointer - symbolIntegral; + } + + float[] syncIntervals = new float[10]; + int adjust = bufferPointer - length + offset; + double pointer = bufferPointer + samplePoint - adjust; + + for(int x = 9; x >= 0; x--) + { + syncIntervals[x] = (float)pointer; + pointer -= mSamplesPerSymbol; + } + + CountDownLatch countDownLatch = new CountDownLatch(1); + getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, + "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain + + " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), countDownLatch); + + try + { + countDownLatch.await(); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } + + /** + * Indicates if the samples in the sample buffer that contain a detected sync pattern have a standard deviation + * that is lower than the expected sample to sample deviation for a modulated signal. False detects against + * noise tend to have a standard deviation that is 2x the expected value. + * @param offset to the sample representing the final symbol in the detected sync pattern. + * @return true if the standard deviation is more than expected. + */ + public double getStandardDeviation(double offset) + { + StandardDeviation standardDeviation = new StandardDeviation(); + int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + end = Math.min(end, mBuffer.length - 1); + + for(int i = start; i < end; i++) + { + standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]); + } + + return standardDeviation.getResult(); + } + + /** + * Symbol timing and equalizer correction candidate settings + */ + public static class Correction + { + private final double mTimingAdjustment; + private final double mAdditionalOffset; + private final float mBalanceAdjustment; + private final float mGainAdjustment; + private final float mDetectionScore; + private final float mOptimizationScore; + private int mCorrectedBitCount = 0; + + /** + * Constructs an instance + * @param timingAdjustment correction for symbol sampling + * @param balanceAdjustment correction for equalizer + * @param gainAdjustment correction for equalizer + * @param bitErrorCount in the detected sync pattern + */ + public Correction(double additionalOffset, double timingAdjustment, float balanceAdjustment, float gainAdjustment, + float detectionScore, float optimizationScore, int bitErrorCount) + { + mAdditionalOffset = additionalOffset; + mTimingAdjustment = timingAdjustment; + mBalanceAdjustment = balanceAdjustment; + mGainAdjustment = gainAdjustment; + mDetectionScore = detectionScore; + mOptimizationScore = optimizationScore; + mCorrectedBitCount = bitErrorCount; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(isValid()) + { + sb.append("\tCORRECTION - Bit Errors [").append(mCorrectedBitCount).append("]"); + } + else + { + sb.append("CORRECTION - INVALID"); + } + + sb.append(" At Offset [").append(mAdditionalOffset); + sb.append("] Adjustments-Timing [").append(mTimingAdjustment); + sb.append("] Balance [").append(mBalanceAdjustment); + sb.append("] Gain [").append(mGainAdjustment); + sb.append("] Detect Score [").append(mDetectionScore); + sb.append("] Optimize Score [").append(mOptimizationScore); + sb.append("]"); + + if(!hasValidTiming()) + { + sb.append(" >> INVALID TIMING"); + } + + return sb.toString(); + } + + /** + * Candidate timing correction + * @return correction + */ + public double getTimingCorrection() + { + return mAdditionalOffset + mTimingAdjustment; + } + + /** + * Equalizer balance correction value. + * @return correction + */ + public float getBalanceAdjustment() + { + return mBalanceAdjustment; + } + + /** + * Equalizer gain correction value + * @return correction. + */ + public float getGainAdjustment() + { + return mGainAdjustment; + } + + /** + * Number of bits that were corrected across the SYNC and NID + * @return corrected bit count. + */ + public int getCorrectedBitCount() + { + return mCorrectedBitCount; + } + + /** + * Indicates if this correction has valid timing adjustment information. + * @return true if the timing correction is not the default (ie no timing) double max value. + */ + public boolean hasValidTiming() + { + return getTimingCorrection() != Double.MAX_VALUE; + } + + /** + * Indicates if this correction has a valid NID or the optimization (or detection) score is high quality. + * @return true if valid. + */ + public boolean isValid() + { + return mOptimizationScore > SYNC_THRESHOLD_EQUALIZED; + } + } + + /** + * Sample equalizer providing gain and balance (DC offset compensation). + * + * Note: equalizer is initialized with a gain value of 1.219, as determined through significant testing against + * both P25 and DMR C4FM modulated samples that employ pulse shaping filters. Gain value is adjusted and + * optimized with each sync detection. + */ + class SampleEqualizer + { + private static final float EQUALIZER_LOOP_GAIN = 0.15f; + private static final float EQUALIZER_MAXIMUM_BALANCE = (float)(Math.PI / 3.0); //+/- 800 Hz + private static final float EQUALIZER_RECALIBRATE_THRESHOLD = (float)(Math.PI / 8.0); + private float mBalance = 0.0f; + private float mGain = 5.7f; + private float mGainInitial = 5.7f; + private float mGainMax = 6.2f; + private float mGainMin = 5.2f; + + /** + * Current balance that represents a traditional PLL signal offset value. + * @return balance. + */ + public float balance() + { + return mBalance; + } + + /** + * Updates the initial, min and max gain values from the samples per symbol + * @param samplesPerSymbol for the incoming stream + */ + public void configure(double samplesPerSymbol) + { + float ratio = (float)samplesPerSymbol / 5.208f; + float gain = 5.7f * ratio; + mGainInitial = gain; + mGain = gain; + mGainMax = gain + 0.5f; + mGainMin = gain - 0.5f; +//TODO: figure out a good formula for initial gain settings .. +// System.out.println("SPS: " + samplesPerSymbol + " Gain - Ratio:" + ratio + " Initial: " + mGainInitial + " Min:" + mGainMin + " Max:" + mGainMax + " Gain" + mGain); + } + + /** + * Reset after center frequency change or PPM change. + */ + public void reset() + { + mSynchronized = false; + mGain = mGainInitial; + } + + /** + * Calculates the mean of sync pattern detection for the samples starting at 9 symbol periods before and ending + * at the tenth symbol. + * @param offset for the sync detection which is just after the sample of the tenth symbol in the sync pattern + * @return mean for the samples in this range + */ + public float getBalance(double offset) + { + Mean mean = new Mean(); + int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + + for(int i = start; i <= end; i++) + { + mean.increment(mBuffer[i]); + } + + return (float)mean.getResult(); + } + + /** + * On sync detection, calculates the optimal timing adjustment that achieves the highest sync correlation score + * or returns an INVALID_SYNC_DETECTION sentinel value if the correlation score doesn't exceed the threshold. + * + * @param additionalOffset from current bufferPointer and samplePoint to adjust for which sync detector + * triggered the event. This can be zero offset for the primary sync detector or a lagging (ie half symbol) + * offset for the lagging sync detector. + * @return optimized timing adjustment or NO_OPTIMIZATION sentinel value. + */ + public Correction optimize(NXDNSyncDetector syncDetector, double additionalOffset, float detectionScore, double bufferOffset) + { + //Offset points to the final symbol/sample in the buffer for the detected sync pattern. + double offset = bufferOffset + additionalOffset; + + //Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold. + double standardDeviation = getStandardDeviation(offset); + + if(standardDeviation > mNoiseStandardDeviationThreshold) + { + return INVALID_SYNC_DETECTION; + } + + float calculatedBalanceCorrection = -getBalance(offset); + + //Find the optimal symbol timing + double samplesPerSymbol = mSamplesPerSymbol; + double stepSize = samplesPerSymbol / (mSynchronized ? 16.0 : 8.0); //Start at 1/8th for coarse & 1/16th for fine + double stepSizeMin = mOptimizeFineIncrement; + double adjustment = 0.0; + + //In coarse sync mode, constrain max adjustment to half a symbol period so that a lagging sync detect doesn't + //preempt a primary sync detect prematurely and cause a significant timing correction. So, we constrain the + //max correction to a half symbol period for coarse sync mode. + double adjustmentMax = samplesPerSymbol / 2.0; + double candidate = offset; + + float scoreCenter = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + + candidate = offset - stepSize; + float scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + + candidate = offset + stepSize; + float scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + + while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax) + { + if(scoreLeft > scoreRight && scoreLeft > scoreCenter) + { + adjustment -= stepSize; + scoreRight = scoreCenter; + scoreCenter = scoreLeft; + + candidate = offset + adjustment - stepSize; + scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + } + else if(scoreRight > scoreLeft && scoreRight > scoreCenter) + { + adjustment += stepSize; + scoreLeft = scoreCenter; + scoreCenter = scoreRight; + + candidate = offset + adjustment + stepSize; + scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + } + else + { + stepSize *= 0.5f; + + if(stepSize > stepSizeMin) + { + candidate = offset + adjustment - stepSize; + scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + + candidate = offset + adjustment + stepSize; + scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection); + } + } + } + + //If we didn't find an optimal correlation score above the threshold, return a false sync. + if(scoreCenter < SYNC_THRESHOLD_OPTIMIZED) + { + return INVALID_SYNC_DETECTION; + } + + return getCorrection(syncDetector, additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset, + calculatedBalanceCorrection); + } + + /** + * Equalize the symbol by applying balance correction and multiplying with gain value. + * @param symbol to equalize. + * @return equalized symbol + */ + public float equalize(float symbol) + { + return symbol * mGain + mBalance; + } + + /** + * Adjusts the symbol balance from the symbol error to address multi-path and fading between sync periods. + */ + public double getAdjustment(float softSymbol, Dibit symbol, int bufferPointer) + { + double adjustment; + + if(softSymbol < symbol.getIdealPhase()) + { + if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1]) + { + adjustment = -mSamplePointAdjustmentIncrement; + } + else + { + adjustment = mSamplePointAdjustmentIncrement; + } + } + else + { + if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1]) + { + adjustment = mSamplePointAdjustmentIncrement; + } + else + { + adjustment = -mSamplePointAdjustmentIncrement; + } + } + + if(Math.abs(mSamplePointAdjustment + adjustment) <= mSamplePointAdjustmentMax) + { + mSamplePointAdjustment += adjustment; + return adjustment; + } + + return 0; + } + + /** + * Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset. + * + * @param sample1 of the sample stream + * @param sample2 of the sample stream + * @param mu interpolation point between sample 1 and sample 2 + * @return equalized symbol + */ + public float getEqualizedSymbol(float sample1, float sample2, double mu) + { + sample1 = equalize(sample1); + sample2 = equalize(sample2); + return LinearInterpolator.calculate(sample1, sample2, mu); + } + + /** + * Gets the interpolated sample value at the specified offset and equalizes it with the current equalizer settings. + * @param offset to the sample + * @return equalized value. + */ + public float getEqualizedSymbol(double offset) + { + int integral = (int)Math.floor(offset); + + if(integral >= 0) + { + return getEqualizedSymbol(mBuffer[integral], mBuffer[integral + 1], offset - integral); + } + + return 0; + } + + /** + * Equalizes both samples using the candidate correction values and returns the interpolated value at the + * mu offset. + * @param sample1 to equalize + * @param sample2 to equalize + * @param mu interpolation between samples 1 and 2 + * @param correction to apply when equalizing samples 1 and 2 + * @return interpolated value. + */ + public float getEqualizedSymbol(float sample1, float sample2, double mu, Correction correction) + { + sample1 = (sample1 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment()); + sample2 = (sample2 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment()); + + if(sample1 > Math.PI) + { + sample1 -= TWO_PI; + } + else if(sample1 < -Math.PI) + { + sample1 += TWO_PI; + } + + if(sample2 > Math.PI) + { + sample2 -= TWO_PI; + } + else if(sample2 < -Math.PI) + { + sample2 += TWO_PI; + } + + return LinearInterpolator.calculate(sample1, sample2, mu); + } + + /** + * Calculates the sync correlation score at the specified offset and samples per symbol interval using the + * current equalizer balance and gain values. + * + * @param offset to the final symbol in the soft symbol buffer + * @param samplesPerSymbol spacing to test for. + * @return correlation score. + */ + public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance) + { + return score(syncDetector, offset, samplesPerSymbol, balance, mGain); + } + + /** + * Calculates the sync correlation score at the specified offset and samples per symbol interval. + * @param offset to the final symbol in the soft symbol buffer + * @param samplesPerSymbol spacing to test for. + * @return correlation score. + */ + public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance, float gain) + { + int maxPointer = mBuffer.length - 1; + float softSymbol; + + double pointer = offset - (samplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1)); + int bufferPointer = (int)Math.floor(pointer); + double fractional = pointer - bufferPointer; + + float score = 0; + float sample1, sample2; + + for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++) + { + if(bufferPointer < maxPointer) + { + sample1 = mBuffer[bufferPointer] * gain + balance; + sample2 = mBuffer[bufferPointer + 1] * gain + balance; + softSymbol = LinearInterpolator.calculate(sample1, sample2, fractional); + } + else + { + softSymbol = 0.0f; + } + + score += softSymbol * syncDetector.getSyncSymbols()[x]; + pointer += samplesPerSymbol; + bufferPointer = (int)Math.floor(pointer); + fractional = pointer - bufferPointer; + } + + return score; + } + + /** + * Applies equalizer correction settings. + * + * @param correction settings to apply + */ + public void apply(Correction correction) + { + float remainingBalanceToCorrect = correction.getBalanceAdjustment() - mBalance; + + //Re-initialize the equalizer any time the balance correction value exceeds the threshold. + if(mSynchronized && Math.abs(remainingBalanceToCorrect) > EQUALIZER_RECALIBRATE_THRESHOLD) + { +// System.out.println("\t\t ***** RESETTING DUE TO EXCESSIVE GAIN IMBALANCE ******* " + remainingBalanceToCorrect); + reset(); + } + + //Limit equalizer adjustments using a control loop at each sync detect after the initial equalizer setup or + //apply as new settings on initial sync detect or any time an excess balance correction is supplied. + if(mSynchronized) + { + mBalance += (remainingBalanceToCorrect * EQUALIZER_LOOP_GAIN); + mGain += (correction.getGainAdjustment() * EQUALIZER_LOOP_GAIN); + } + else + { + mBalance += remainingBalanceToCorrect; + mGain += correction.getGainAdjustment(); + } + + mBalance = Math.min(mBalance, EQUALIZER_MAXIMUM_BALANCE); + mBalance = Math.max(mBalance, -EQUALIZER_MAXIMUM_BALANCE); + mGain = Math.min(mGain, mGainMax); + mGain = Math.max(mGain, mGainMin); + } + + /** + * Calculates a correction update for the equalizer balance and gain when the sync pattern is detected in the + * sample buffer, using the supplied timing correction argument. Resamples the sync symbols and compares each + * soft symbol to the ideal symbol phase to develop average error measurements for balance and gain. On initial + * sync detection, the equalizer settings are applied to the samples in the buffer allowing the symbols to be + * resampled during coarse sync acquisition. + */ + public Correction getCorrection(NXDNSyncDetector syncDetector, double additionalOffset, double timingCorrection, + float detectionScore, float optimizationScore, double offset, float balanceCorrection) + { + double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1)); + int resampleStartIntegral = (int)Math.floor(resampleStart); + float symbol, gainAccumulator = 0, resampledSoftSymbol; + Dibit resampledDibit; + int bitErrorCount = 0; + + for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++) + { + if(resampleStartIntegral >= 0) + { + symbol = syncDetector.getSyncSymbols()[x]; + resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], + mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); + resampledSoftSymbol = resampledSoftSymbol * mGain + balanceCorrection; + resampledDibit = toSymbol(resampledSoftSymbol); + bitErrorCount += syncDetector.getSyncDibits()[x].getBitErrorFrom(resampledDibit); + gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol); + } + + resampleStart += mSamplesPerSymbol; + resampleStartIntegral = (int)Math.floor(resampleStart); + } + + gainAccumulator /= syncDetector.getSyncPatternDibitLength(); + + //Recalculate the optimization score using the adjusted equalizer settings + if(!mSynchronized) + { + double optimizedFinalSymbol = offset + additionalOffset + timingCorrection; + balanceCorrection = -getBalance(optimizedFinalSymbol); + optimizationScore = score(syncDetector, optimizedFinalSymbol, mSamplesPerSymbol, + balanceCorrection, mGain + gainAccumulator); + } + + return new Correction(additionalOffset, timingCorrection, balanceCorrection, gainAccumulator, detectionScore, + optimizationScore, bitErrorCount); + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java new file mode 100644 index 00000000..1362e55e --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java @@ -0,0 +1,794 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.controller.channel.ChannelEvent; +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; +import io.github.dsheirer.identifier.alias.TalkerAliasManager; +import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.module.decode.event.DecodeEvent; +import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector; +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.nxdn.channel.ChannelFrequency; +import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel; +import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelFake; +import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkerAliasIdentifier; +import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic; +import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec; +import io.github.dsheirer.module.decode.nxdn.layer3.type.CallTimer; +import io.github.dsheirer.module.decode.nxdn.layer3.type.CallType; +import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation; +import io.github.dsheirer.module.decode.nxdn.layer3.type.DataCallOption; +import io.github.dsheirer.module.decode.nxdn.layer3.type.Duplex; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; +import io.github.dsheirer.module.decode.nxdn.layer3.type.VoiceCallOption; +import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.config.SourceConfigTuner; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages trunked system traffic channel activations + */ +public class NXDNTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, + IChannelEventListener, IChannelEventProvider +{ + private static final Logger LOGGER = LoggerFactory.getLogger(NXDNTrafficChannelManager.class); + public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED"; + public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; + private final Channel mParentChannel; + private final DecodeEventDuplicateDetector mDuplicateDetector = new DecodeEventDuplicateDetector(); + private final List mChannelFrequencies = new ArrayList<>(); + private final Lock mLock = new ReentrantLock(); + private final Map mAllocatedTrafficChannelMap = new HashMap<>(); + private final Map mEventTrackerMap = new HashMap<>(); + private final Queue mAvailableTrafficChannelQueue = new LinkedTransferQueue<>(); + private final TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager(); + private final TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor(); + private List mManagedTrafficChannels; + private Listener mDecodeEventListener; + private Listener mChannelEventListener; + private ChannelAccessInformation mChannelAccessInformation; + private boolean mIgnoreDataCalls; + private boolean mIgnoreEncryptedCalls; + + /** + * Constructs an instance + * + * @param parentChannel configuration for the parent channel + */ + public NXDNTrafficChannelManager(Channel parentChannel) + { + mParentChannel = parentChannel; + + if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN) + { + mIgnoreDataCalls = configNXDN.isIgnoreDataCalls(); + mIgnoreEncryptedCalls = configNXDN.isIgnoreEncryptedCalls(); + mChannelFrequencies.addAll(configNXDN.getChannelMap()); + createTrafficChannels(configNXDN); + } + } + + /** + * Talker alias manager + */ + public TalkerAliasManager getTalkerAliasManager() + { + return mTalkerAliasManager; + } + + /** + * Sets the channel access info from the control channel. + * + * @param info from the control channel + */ + public void setChannelAccessInformation(ChannelAccessInformation info) + { + if(mChannelAccessInformation == null) + { + mChannelAccessInformation = info; + } + } + + /** + * Creates up to the maximum number of traffic channels for use in allocating traffic channels. + * + * @param decodeConfig to use for each traffic channel + */ + private void createTrafficChannels(DecodeConfigNXDN decodeConfig) + { + if(mManagedTrafficChannels == null) + { + int trafficChannelPoolSize = decodeConfig.getTrafficChannelPoolSize(); + List trafficChannelList = new ArrayList<>(); + + if(trafficChannelPoolSize > 0) + { + for(int x = 0; x < trafficChannelPoolSize; x++) + { + Channel trafficChannel = new Channel("T-" + mParentChannel.getName(), Channel.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); + } + } + + /** + * Retrieves the current event tracker for the specified channel and if the tracker is stale relative to the + * timestamp, returns null, otherwise returns the current tracker. + * + * @param frequency to look up the tracker + * @param timestamp to compare for staleness + * @return tracker or null + */ + private NXDNChannelEventTracker getTrackerRemoveIfStale(long frequency, long timestamp) + { + NXDNChannelEventTracker tracker = mEventTrackerMap.get(frequency); + + if(tracker != null && tracker.isStale(timestamp)) + { + removeTracker(frequency); + tracker = null; + } + + return tracker; + } + + /** + * Broadcasts the decode event from the tracker. + * + * @param tracker containing a decode event. + */ + public void broadcast(NXDNChannelEventTracker tracker) + { + broadcast(tracker.getEvent()); + } + + /** + * Broadcasts an initial or update decode event to any registered listener. + */ + public void broadcast(DecodeEvent decodeEvent) + { + if(mDecodeEventListener != null) + { + if(decodeEvent.getEventType() == DecodeEventType.DATA_CALL && mDuplicateDetector.isDuplicate(decodeEvent, + System.currentTimeMillis())) + { + return; + } + + mDecodeEventListener.receive(decodeEvent); + } + else + { + System.out.println("Decode event listener is null"); + } + } + + /** + * Selects the decode event type based on the call type and encryption. + * + * @param callType for the call + * @param encryption for the call + * @return decode event type + */ + private DecodeEventType getType(CallType callType, EncryptionKeyIdentifier encryption) + { + boolean encrypted = encryption.isEncrypted(); + return switch(callType) + { + case GROUP_BROADCAST, GROUP_CONFERENCE -> + encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; + case INDIVIDUAL -> + encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; + case INTERCONNECT, SPEED_DIAL -> + encrypted ? DecodeEventType.CALL_INTERCONNECT_ENCRYPTED : DecodeEventType.CALL_INTERCONNECT; + default -> encrypted ? DecodeEventType.CALL_ENCRYPTED : DecodeEventType.CALL; + }; + } + + /** + * Creates a traffic channel event tracker + * + * @param eventType for the decode event + * @param ic collection of from/to identifiers + * @param channel to track + * @param timestamp for the event + * @return tracker + */ + private NXDNChannelEventTracker createTracker(DecodeEventType eventType, IdentifierCollection ic, + NXDNChannel channel, long timestamp) + { + long frequency = channel != null ? channel.getDownlinkFrequency() : 0; + + DecodeEvent event = NXDNDecodeEvent.builder(eventType, timestamp) + .timeslot(0) + .channel(channel) + .identifiers(ic) + .build(); + NXDNChannelEventTracker tracker = new NXDNChannelEventTracker(event); + mEventTrackerMap.put(frequency, tracker); + return tracker; + } + + /** + * Process a data call assignment + * + * @param dca to process + */ + public void processDataCallAssignment(DataCallAssignment dca) + { + if(dca.hasChannel() && dca.getChannel().getDownlinkFrequency() > 0) + { + DecodeEventType eventType = dca.getEncryptionKeyIdentifier().isEncrypted() ? + DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL; + processDataCall(dca.getIdentifiers(), dca.getChannel(), eventType, dca.getTimestamp(), dca.getCallOption(), + dca.getCallTimer()); + } + } + + /** + * Process a voice call assignment or notification + * @param identifiers for the call + * @param channel for the call + */ + private void processDataCall(List identifiers, NXDNChannel channel, DecodeEventType eventType, + long timestamp, DataCallOption dco, CallTimer callTimer) + { + if(channel.getDownlinkFrequency() > 0) + { + mLock.lock(); + + try + { + NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(channel.getDownlinkFrequency(), timestamp); + MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers); + if(tracker != null) + { + if(tracker.isSameCallCheckingToAndFrom(ic, timestamp)) + { + tracker.updateDurationControl(timestamp); + broadcast(tracker); + } + else + { + removeTracker(channel.getDownlinkFrequency()); + tracker = null; + } + } + + if(tracker == null) + { + tracker = createTracker(eventType, ic, channel, timestamp); + Duplex duplex = dco.getDuplex(); + TransmissionMode mode = dco.getTransmissionMode(); + tracker.addDetails("TIMER:" + callTimer + " " + duplex + " " + mode); + } + + if(mIgnoreDataCalls) + { + tracker.prefixDetails("IGNORED DATA"); + } + else + { + if(!tracker.isTrafficChannelAllocated() && channel.isValid() && + channel.getDownlinkFrequency() != getCurrentControlFrequency() && + !mAllocatedTrafficChannelMap.containsKey(channel.getDownlinkFrequency())) + { + //Retrieve a channel from the traffic channel queue + Channel traffic = mAvailableTrafficChannelQueue.poll(); + + if(traffic != null) + { + requestTrafficChannelStart(traffic, channel, ic, tracker); + } + else + { + tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails()); + } + } + } + + broadcast(tracker); + } + finally + { + mLock.unlock(); + } + } + } + + /** + * Process a voice call assignment indicating there is a call on another channel. + * + * @param vca to process + */ + public void processVoiceCallAssignment(VoiceCallAssignment vca) + { + if(vca.hasChannel() && vca.getChannel().getDownlinkFrequency() > 0) + { + processVoiceCall(vca.getIdentifiers(), vca.getChannel(), vca.getCallType(), vca.getEncryptionKeyIdentifier(), + vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer()); + } + } + + /** + * Process a voice call assignment duplicate notification from a traffic channel + * + * @param vca to process + */ + public void processVoiceCallAssignment(VoiceCallAssignmentDuplicateTraffic vca) + { + NXDNChannel channel = vca.getChannel(); + + if(channel == null || channel.getDownlinkFrequency() == 0) + { + //If the channel is null or not configured, use the talkgroup value so we can track the call event + channel = new NXDNChannelFake(vca.getDestination().getValue()); + } + + processVoiceCall(vca.getIdentifiers(), channel, vca.getCallType(), vca.getEncryptionKeyIdentifier(), + vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer()); + } + + /** + * Process a traffic channel voice call message + * @param voiceCall message + * @param channel for the call, can be null + */ + public void processVoiceCall(VoiceCall voiceCall, IChannelDescriptor channel) + { + NXDNChannel nxdn = null; + if(channel instanceof NXDNChannel nxdnChannel) + { + nxdn = nxdnChannel; + } + + processVoiceCall(voiceCall.getIdentifiers(), nxdn, voiceCall.getCallType(), voiceCall.getEncryptionKeyIdentifier(), + voiceCall.getTimestamp(), voiceCall.getCallOption(), CallTimer.UNSPECIFIED); + } + + /** + * Adds or updates the talker alias for the tracked call event. + * @param channel for the call + * @param talkerAlias to add or update + * @param timestamp for the talker alias message + */ + public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias, + RadioIdentifier radio, long timestamp) + { + mLock.lock(); + + try + { + long frequency = channel != null ? channel.getDownlinkFrequency() : 0; + NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp); + + if(tracker != null) + { + tracker.addIdentifierIfMissing(talkerAlias); + broadcast(tracker); + } + + getTalkerAliasManager().update(radio, talkerAlias); + } + finally + { + mLock.unlock(); + } + } + + /** + * Updates the end time for an ongoing call to reflect progress + * @param channel descriptor for the traffic channel + */ + public void processCallProgressUpdate(IChannelDescriptor channel, long timestamp) + { + mLock.lock(); + try + { + long frequency = channel != null ? channel.getDownlinkFrequency() : 0; + NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp); + + if(tracker != null) + { + tracker.updateDurationTraffic(timestamp); + broadcast(tracker); + } + } + finally + { + mLock.unlock(); + } + } + + /** + * Ends the current call and removes the event tracker + * @param channel for the call + * @param timestamp for the end call + */ + public void processEndCall(IChannelDescriptor channel, long timestamp) + { + mLock.lock(); + + try + { + long frequency = channel != null ? channel.getDownlinkFrequency() : 0; + NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp); + + if(tracker != null) + { + tracker.completeTraffic(timestamp); + broadcast(tracker); + removeTracker(frequency); + } + } + finally + { + mLock.unlock(); + } + } + + /** + * Process a voice call assignment or notification + * @param identifiers for the call + * @param channel for the call + */ + private void processVoiceCall(List identifiers, NXDNChannel channel, CallType callType, + EncryptionKeyIdentifier encryption, long timestamp, VoiceCallOption vco, + CallTimer callTimer) + { + mLock.lock(); + + long frequency = channel != null ? channel.getDownlinkFrequency() : 0; + + try + { + NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp); + MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers); + mTalkerAliasManager.enrich(ic); + if(tracker != null) + { + if(tracker.isSameCallCheckingToAndFrom(ic, timestamp)) + { + tracker.updateDurationControl(timestamp); + } + else + { + removeTracker(frequency); + tracker = null; + } + } + + if(tracker == null) + { + DecodeEventType eventType = getType(callType, encryption); + tracker = createTracker(eventType, ic, channel, timestamp); + AudioCodec audioCodec = vco.getCodec(); + TransmissionMode mode = vco.getTransmissionMode(); + + if(encryption.isEncrypted()) + { + tracker.addDetails(encryption + " TIMER:" + callTimer + " " + audioCodec + " " + mode); + } + else + { + tracker.addDetails("TIMER:" + callTimer + " " + audioCodec + " " + mode); + } + } + + boolean ignoreEncrypted = mIgnoreEncryptedCalls && encryption.isEncrypted(); + + if(ignoreEncrypted) + { + tracker.prefixDetails("IGNORED ENCRYPTED"); + } + else if(!tracker.isTrafficChannelAllocated() && channel != null && channel.isValid() && + channel.getDownlinkFrequency() != getCurrentControlFrequency() && + !mAllocatedTrafficChannelMap.containsKey(frequency)) + { + //Retrieve a channel from the traffic channel queue + Channel traffic = mAvailableTrafficChannelQueue.poll(); + + if(traffic != null) + { + requestTrafficChannelStart(traffic, channel, ic, tracker); + } + else + { + tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails()); + } + } + + broadcast(tracker); + } + finally + { + mLock.unlock(); + } + } + + /** + * Sends a channel start request to the ChannelProcessingManager. + * Note: this method is not thread safe and the calling method must protect access using mLock. + * + * @param trafficChannel to use for the traffic channel + * @param nxdnChannel that describes the traffic channel downlink frequency + * @param ic containing identifiers for the call + * @param tracker to update with channel allocation flag + */ + private void requestTrafficChannelStart(Channel trafficChannel, NXDNChannel nxdnChannel, + IdentifierCollection ic, NXDNChannelEventTracker tracker) + { + if(nxdnChannel != null && nxdnChannel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null) + { + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency(nxdnChannel.getDownlinkFrequency()); + if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) + { + sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); + } + trafficChannel.setSourceConfiguration(sourceConfig); + mAllocatedTrafficChannelMap.put(nxdnChannel.getDownlinkFrequency(), trafficChannel); + ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel, + nxdnChannel, ic, this); + startChannelRequest.addPreloadDataContent(new NXDNChannelInfoPreloadData(mChannelAccessInformation, mChannelFrequencies)); + getInterModuleEventBus().post(startChannelRequest); + tracker.setTrafficChannelAllocated(true); + } + else + { + //Return the channel to the traffic channel pool if we didn't start it. + mAvailableTrafficChannelQueue.add(trafficChannel); + } + } + + @Override + public Listener getChannelEventListener() + { + return mTrafficChannelTeardownMonitor; + } + + /** + * Broadcasts the channel event ot an optionally registered listener + * @param event to broadcast + */ + private void broadcast(ChannelEvent event) + { + if(mChannelEventListener != null) + { + mChannelEventListener.receive(event); + } + } + + @Override + public void setChannelEventListener(Listener listener) + { + mChannelEventListener = listener; + } + + @Override + public void removeChannelEventListener() + { + mChannelEventListener = null; + } + + /** + * Process the current control frequency to ensure we don't allocated traffic channels for the same frequency. + * @param previous frequency for the control channel (to remove from allocated channels) + * @param current frequency for the control channel (to add to allocated channels) + * @param channel for the current control channel + */ + @Override + protected void processControlFrequencyUpdate(long previous, long current, Channel channel) + { + mLock.lock(); + + try + { + //Clear any traffic channel that is already allocated on the new/current control frequency + if(mAllocatedTrafficChannelMap.containsKey(current)) + { + broadcast(new ChannelEvent(mAllocatedTrafficChannelMap.get(current), ChannelEvent.Event.REQUEST_DISABLE)); + } + } + finally + { + mLock.unlock(); + } + + } + + @Override + public void reset() + { + + } + + @Override + public void start() + { + + } + + @Override + public void stop() + { + mLock.lock(); + + try + { + mAvailableTrafficChannelQueue.clear(); + + List channels = new ArrayList<>(mAllocatedTrafficChannelMap.values()); + + //Issue a disable request for each traffic channel + for(Channel channel: channels) + { + LOGGER.info("Stopping NXDN traffic channel: " + channel); + broadcast(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_DISABLE)); + } + } + finally + { + mLock.unlock(); + } + } + + @Override + public void addDecodeEventListener(Listener listener) + { + mDecodeEventListener = listener; + } + + @Override + public void removeDecodeEventListener(Listener listener) + { + mDecodeEventListener = null; + } + + /** + * Remotes the event tracker from the tracker map + * @param frequency for the tracker + */ + private void removeTracker(long frequency) + { + mLock.lock(); + + try + { + mEventTrackerMap.remove(frequency); + } + finally + { + mLock.unlock(); + } + } + + /** + * Monitors channel teardown events to detect when traffic channel processing has ended or channel start has been + * rejected. Reclaims the traffic channel instance for reuse by future traffic channel grants. + */ + public class TrafficChannelTeardownMonitor implements Listener + { + /** + * Process channel events from the ChannelProcessingManager to account for owned child traffic channels. + * Note: this method sees events for ALL channels and not just P25 channels managed by this instance. + * + * @param channelEvent to process + */ + @Override + public void receive(ChannelEvent channelEvent) + { + Channel channel = channelEvent.getChannel(); + + if(mManagedTrafficChannels.contains(channel)) + { + mLock.lock(); + + try + { + switch(channelEvent.getEvent()) + { + case NOTIFICATION_PROCESSING_STOP: + mAllocatedTrafficChannelMap.entrySet() + .stream() + .filter(entry -> entry.getValue() == channel) + .map(Map.Entry::getKey) + .findFirst() + .ifPresent(frequency -> { + mAllocatedTrafficChannelMap.remove(frequency); + //Don't remove the tracker. There's a chance the control channel can still + //reference the now terminating traffic channel and cause a residual event + //creation and channel allocation and we'll use the tracker to keep that from + //happening. + //removeTracker(frequency); + mAvailableTrafficChannelQueue.add(channel); + }); + break; + case NOTIFICATION_PROCESSING_START_REJECTED: + mAllocatedTrafficChannelMap.entrySet().stream() + .filter(entry -> entry.getValue() == channel) + .map(Map.Entry::getKey) + .findFirst() + .ifPresent(rejectedFrequency -> { + mAllocatedTrafficChannelMap.remove(rejectedFrequency); + mAvailableTrafficChannelQueue.add(channel); + + //Leave the event in the map so that it doesn't get recreated. The channel + //processing manager set the 'tuner not available' in the details already + NXDNChannelEventTracker tracker = mEventTrackerMap.get(rejectedFrequency); + + if(tracker != null) + { + if(!tracker.getEvent().getDetails().contains(CHANNEL_START_REJECTED)) + { + tracker.addDetails(CHANNEL_START_REJECTED + " " + channelEvent.getDescription() + + (tracker.getEvent().getDetails() != null ? " - " + tracker.getEvent().getDetails() : "")); + } + tracker.setTrafficChannelAllocated(false); + broadcast(tracker); + } + }); + break; + } + } + finally + { + mLock.unlock(); + } + } + } + } + +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java new file mode 100644 index 00000000..4149940f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java @@ -0,0 +1,79 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn; + +/** + * Tracks sync detections for fixed length messages + */ +public class SyncTracker +{ + private final int mSymbolsPerMessage; + private final int mSyncLossThreshold; + private final int mMaxSyncWindowSize; + private int mSyncCount = 0; + private int mSymbolCount; + + /** + * Constructs an instance + * @param symbolsPerMessage a count of how many symbols per message, between each sync interval (ie message length). + * @param messageWindowSize window size for retaining sync across messages sequences + */ + public SyncTracker(int symbolsPerMessage, int messageWindowSize) + { + mSymbolsPerMessage = symbolsPerMessage; + mSyncLossThreshold = symbolsPerMessage * 2; + mMaxSyncWindowSize = messageWindowSize; + } + + /** + * Increments the symbol count. + */ + public void symbol() + { + mSymbolCount++; + + if(mSymbolCount >= mSyncLossThreshold) + { + mSymbolCount -= mSymbolsPerMessage; + mSyncCount--; + mSyncCount = Math.max(mSyncCount, 0); + } + } + + /** + * Indicates when a sync pattern is detected. + */ + public void syncDetected() + { + mSyncCount++; + mSyncCount = Math.min(mSyncCount, mMaxSyncWindowSize); + mSymbolCount -= mSymbolsPerMessage; + mSymbolCount = Math.max(mSymbolCount, 0); + } + + /** + * Indicates if there has been at least one sync detection in the past maxSyncSequence message windows. + * @return true if there is at least one sync detection + */ + public boolean isSynchronized() + { + return mSyncCount > 0; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java new file mode 100644 index 00000000..ae61a9d1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java @@ -0,0 +1,175 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.nxdn.audio; + +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.dsp.gain.NonClippingGain; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio; +import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect; +import io.github.dsheirer.module.decode.nxdn.layer3.call.TransmissionRelease; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall; +import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallWithOptionalLocation; +import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.sample.Listener; +import java.util.ArrayList; +import java.util.List; + +/** + * NXDN AMBE audio module + */ +public class NXDNAudioModule extends AmbeAudioModule +{ + private final SquelchStateListener mSquelchStateListener = new SquelchStateListener(); + private final NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); + private final List