diff --git a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java index b2814c7b4..5df660126 100644 --- a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java @@ -1,35 +1,33 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** */ package io.github.dsheirer.identifier; import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier; +import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Identifier collection with methods for changing or updating managed identifiers @@ -209,6 +207,15 @@ public void update(Identifier identifier) remove(existing); add(identifier); } + //Always replace a radio identifier with a fully qualified variant of itself + else if(existing instanceof RadioIdentifier && + !(existing instanceof FullyQualifiedRadioIdentifier) && + identifier instanceof FullyQualifiedRadioIdentifier && + existing.getValue().equals(identifier.getValue())) + { + remove(existing); + add(identifier); + } } else { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 905133b09..94a4b6106 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -31,8 +31,10 @@ 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.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent; import io.github.dsheirer.identifier.scramble.ScrambleParameterIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.log.LoggingSuppressor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageListener; @@ -709,6 +711,78 @@ public void processP1ControlDirectedChannelGrant(APCO25Channel apco25Channel, Se } } + /** + * Process a Phase 1 HDU indicating a call start. + * + * @param frequency for the call event + * @param talkgroup to update within the event. + * @param eki for encryption settings. + * @param timestamp for the update + */ + public void processP1TrafficCallStart(long frequency, Identifier talkgroup, Identifier radio, + EncryptionKeyIdentifier eki, ServiceOptions serviceOptions, + IChannelDescriptor channelDescriptor, long timestamp) + { + mLock.lock(); + + try + { + P25TrafficChannelEventTracker tracker = getTracker(frequency, P25P1Message.TIMESLOT_1); + + if(tracker != null) + { + removeTracker(frequency, P25P1Message.TIMESLOT_1); + } + + DecodeEventType decodeEventType = getDecodeEventType(talkgroup, eki); + + MutableIdentifierCollection mic = new MutableIdentifierCollection(); + mic.update(talkgroup); + mic.update(radio); + mic.update(eki); + + //Create a new event for the current call. + P25ChannelGrantEvent callEvent = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + .channelDescriptor(channelDescriptor) + .details("PHASE 1 CALL " + (serviceOptions != null ? serviceOptions : "")) + .identifiers(mic) + .build(); + + tracker = new P25TrafficChannelEventTracker(callEvent); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); + broadcast(tracker); + } + finally + { + mLock.unlock(); + } + } + + /** + * Determines the HDU message call decode event type from the talkgroup identifier and encryption key + * @param talkgroup to inspect + * @param eki to inspect + * @return decode event type. + */ + private static DecodeEventType getDecodeEventType(Identifier talkgroup, EncryptionKeyIdentifier eki) + { + DecodeEventType decodeEventType = null; + + if(talkgroup instanceof PatchGroupIdentifier) + { + decodeEventType = eki.isEncrypted() ? DecodeEventType.CALL_PATCH_GROUP_ENCRYPTED : DecodeEventType.CALL_PATCH_GROUP; + } + else if(talkgroup instanceof TalkgroupIdentifier ti && ti.getValue() == 0) //Unit-to-Unit private call + { + decodeEventType = eki.isEncrypted() ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; + } + else + { + decodeEventType = eki.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; + } + return decodeEventType; + } + /** * Updates an identifier for an ongoing call event on the frequency and updates the event duration timestamp. * @@ -755,6 +829,55 @@ public void processP1TrafficCurrentUser(long frequency, Identifier identifier, l } } + /** + * Updates all current identifiers for an ongoing call event on the frequency and updates the event duration timestamp. + * + * Note: if this manager does not have an existing call event for the frequency, the update is ignored + * because we don't have enough detail to create a call event. + * + * This is used primarily to add encryption, GPS, talker alias, etc. but can be used for any identifier update. + * + * @param frequency for the call event + * @param identifier to update within the event. + * @param timestamp for the update + */ + public void processP1TrafficCurrentUserIdentifiers(long frequency, List identifiers, long timestamp, String context) + { + mLock.lock(); + + try + { + P25TrafficChannelEventTracker tracker = getTracker(frequency, P25P1Message.TIMESLOT_1); + + if(tracker != null && tracker.isComplete()) + { + removeTracker(frequency, P25P1Message.TIMESLOT_1); + tracker = null; + } + + if(tracker != null) + { + for(Identifier identifier : identifiers) + { + tracker.addIdentifierIfMissing(identifier); + + //Add the encryption key to the call event details. + if(identifier instanceof EncryptionKeyIdentifier eki && eki.isEncrypted()) + { + tracker.addDetailsIfMissing(eki.toString()); + } + } + + tracker.updateDurationTraffic(timestamp); + broadcast(tracker); + } + } + finally + { + mLock.unlock(); + } + } + /** * Processes traffic channel announced current user information. * @param frequency of the traffic channel diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1CallSequenceRecorder.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1CallSequenceRecorder.java index 1524822ab..0e4c029da 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1CallSequenceRecorder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1CallSequenceRecorder.java @@ -39,7 +39,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU2Message; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDUMessage; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage; import io.github.dsheirer.preference.UserPreferences; import java.util.List; @@ -119,9 +119,9 @@ public void process(P25P1Message message) { process((LDUMessage)message); } - else if(message instanceof TDULinkControlMessage) + else if(message instanceof TDULCMessage) { - process((TDULinkControlMessage)message); + process((TDULCMessage)message); } else if(message instanceof TDUMessage) { @@ -133,7 +133,7 @@ else if (message instanceof HDUMessage) } } - private void process(TDULinkControlMessage tdulc) + private void process(TDULCMessage tdulc) { process(tdulc.getLinkControlWord()); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java index 20fb940d1..a96cac45e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java @@ -61,7 +61,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.IExtendedSourceMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete; @@ -113,7 +112,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDCPPacketMessage; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.response.ResponseMessage; import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.isp.UMBTCTelephoneInterconnectRequestExplicitDialing; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.Opcode; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.L3HarrisGroupRegroupExplicitEncryptionCommand; @@ -190,6 +189,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList private final P25P1NetworkConfigurationMonitor mNetworkConfigurationMonitor; private final Listener mChannelEventListener; private P25TrafficChannelManager mTrafficChannelManager; + private ServiceOptions mCurrentServiceOptions; /** * Constructs an APCO-25 decoder state with an optional traffic channel manager. @@ -316,10 +316,6 @@ public void receive(IMessage iMessage) break; } } - else if(iMessage instanceof IExtendedSourceMessage esm && iMessage instanceof LinkControlWord lcw) - { - processLC(lcw, esm.getTimestamp(), esm.isTerminator()); - } else if(iMessage instanceof MotorolaTalkerAliasComplete tac) { mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias()); @@ -413,7 +409,6 @@ private void processLCChannelUser(LinkControlWord lcw, long timestamp) getIdentifierCollection().update(updated); DecodeEventType decodeEventType = getLCDecodeEventType(lcw); - ServiceOptions serviceOptions = null; if(lcw instanceof IServiceOptionsProvider sop) @@ -831,17 +826,31 @@ private void processHDU(IMessage message) if(message.isValid() && message instanceof HDUMessage hdu) { HeaderData headerData = hdu.getHeaderData(); - //Run the talkgroup through the patch group manager so we don't get a plain talkgroup in addition to the patch - Identifier talkgroup = mPatchGroupManager.update(headerData.getTalkgroup(), message.getTimestamp()); - mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), talkgroup, hdu.getTimestamp(), message.toString()); - if(headerData.isEncryptedAudio()) - { - broadcast(new DecoderStateEvent(this, Event.START, State.ENCRYPTED)); - } - else + if(headerData.isValid()) { - broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); + Identifier talkgroup = headerData.getTalkgroup(); + + //Run the talkgroup through the patch group manager so we don't get a plain talkgroup in addition to the patch + //Talkgroup value of zero indicates a unit-to-unit call, so don't attempt to update it as a patch group + if(headerData.getTalkgroup().getValue() > 0) + { + talkgroup = mPatchGroupManager.update(talkgroup, message.getTimestamp()); + } + + Identifier radio = getIdentifierCollection().getFromIdentifier(); + + mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio, + headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp()); + + if(headerData.isEncryptedAudio()) + { + broadcast(new DecoderStateEvent(this, Event.START, State.ENCRYPTED)); + } + else + { + broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); + } } } } @@ -862,6 +871,8 @@ private void processLDU(P25P1Message message) if(lcw != null && lcw.isValid()) { processLC(lcw, message.getTimestamp(), false); + mTrafficChannelManager.processP1TrafficCurrentUserIdentifiers(getCurrentFrequency(), + getIdentifierCollection().getIdentifiers(), message.getTimestamp(), ldu1.toString()); } } else if(message instanceof LDU2Message ldu2) @@ -917,7 +928,7 @@ private void processTDULC(P25P1Message message) getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); } - if(message instanceof TDULinkControlMessage tdulc) + if(message instanceof TDULCMessage tdulc) { LinkControlWord lcw = tdulc.getLinkControlWord(); @@ -1831,7 +1842,10 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator { switch(lcw.getOpcode()) { - //Calls in-progress on this channel + case SOURCE_ID_EXTENSION: + //Ignore - handled elsewhere + break; + //Calls getting ready to start or in-progress on this channel case GROUP_VOICE_CHANNEL_USER: case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER: case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER: @@ -1839,7 +1853,9 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED: if(isTerminator) { - closeCurrentCallEvent(timestamp, lcw.toString()); + mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), timestamp, lcw.toString()); + List updated = mPatchGroupManager.update(lcw.getIdentifiers(), timestamp); + getIdentifierCollection().update(updated); } else { @@ -1941,11 +1957,6 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator getIdentifierCollection().update(frequencyID); } - if(isTerminator) - { - closeCurrentCallEvent(timestamp, lcw.toString()); - } - mNetworkConfigurationMonitor.process(lcw); break; case NETWORK_STATUS_BROADCAST_EXPLICIT: @@ -2162,9 +2173,6 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator closeCurrentCallEvent(timestamp, lcw.toString()); } break; - case SOURCE_ID_EXTENSION: - //Ignore - handled elsewhere - break; default: if(isTerminator) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java index 9ee36a87d..882398fca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java @@ -19,10 +19,12 @@ package io.github.dsheirer.module.decode.p25.phase1; import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.message.AbstractMessage; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver; +import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.IExtendedSourceMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.HarrisTalkerAliasAssembler; @@ -30,7 +32,8 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkerAliasAssembler; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU2Message; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import io.github.dsheirer.sample.Listener; import java.util.List; import java.util.Map; @@ -59,9 +62,12 @@ public class P25P1MessageProcessor implements Listener private Map mFrequencyBandMap = new TreeMap(); /** - * Temporary holding of an extended source link control message while it awaits the extension message to arrive. + * Temporary holding for an extended source link control message while it awaits the extension message to arrive. */ - private IExtendedSourceMessage mExtendedSourceMessage; + private TDULCMessage mHeldTDULCMessage; + private HDUMessage mHeldHDUMessage; + private LDU1Message mHeldLDU1Message; + private LDU2Message mHeldLDU2Message; /** * Motorola talker alias assembler for link control header and data blocks. @@ -103,120 +109,200 @@ public void preload(P25FrequencyBandPreloadDataContent content) @Override public void receive(IMessage message) { - //Optional message created during processing that should be sent after the current argument is sent. - IMessage additionalMessageToSend = null; - if(message.isValid()) { //Reassemble extended source link control messages. - if(message instanceof LDU1Message ldu) + if(message instanceof LDU1Message ldu1) { - if(ldu.getLinkControlWord() instanceof IExtendedSourceMessage extendedSourceMessage) + LinkControlWord lcw = ldu1.getLinkControlWord();; + + if(lcw instanceof IExtendedSourceMessage esm) { - additionalMessageToSend = reassembleLC(extendedSourceMessage); + if(lcw instanceof LCSourceIDExtension extension) + { + processSourceIDExtension(extension); + } + else if(esm.isExtensionRequired()) + { + processSourceIDExtension(null); + mHeldLDU1Message = ldu1; + return; + } } - else if(ldu.getLinkControlWord() instanceof LCHarrisTalkerAliasBase harrisTalkerAlias) + else if(lcw instanceof LCHarrisTalkerAliasBase harrisTalkerAlias) { //Send the LCW to the harris talker alias assembler - additionalMessageToSend = mHarrisTalkerAliasAssembler.process(harrisTalkerAlias, ldu.getTimestamp()); + dispatch(mHarrisTalkerAliasAssembler.process(harrisTalkerAlias, ldu1.getTimestamp())); } + + //Flush any held messages + processSourceIDExtension(null); + + dispatch(ldu1); } - else if(message instanceof TDULinkControlMessage tdu) + else if(message instanceof LDU2Message ldu2) { - LinkControlWord lcw = tdu.getLinkControlWord(); - - if(lcw instanceof IExtendedSourceMessage extendedSourceMessage) + //If we held onto an LDU1 awaiting a source ID extension, then also hold onto this LDU2 and flush them in sequence. + if(mHeldLDU1Message != null) { - additionalMessageToSend = reassembleLC(extendedSourceMessage); + mHeldLDU2Message = ldu2; } - //Motorola carries the talker alias in the TDULC - else if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp())) + else { - additionalMessageToSend = mMotorolaTalkerAliasAssembler.assemble(); + dispatch(ldu2); } - - //Harris carries the talker alias in the LDU voice messages, so reset the assembler on TDULC. - mHarrisTalkerAliasAssembler.reset(); } - - //Insert frequency band identifier update messages into channel-type messages */ - if(message instanceof IFrequencyBandReceiver) + else if(message instanceof TDULCMessage tdulc) { - IFrequencyBandReceiver receiver = (IFrequencyBandReceiver)message; - - List channels = receiver.getChannels(); + LinkControlWord lcw = tdulc.getLinkControlWord(); - for(IChannelDescriptor channel : channels) + if(lcw instanceof IExtendedSourceMessage esm) { - int[] frequencyBandIdentifiers = channel.getFrequencyBandIdentifiers(); - - for(int id : frequencyBandIdentifiers) + if(lcw instanceof LCSourceIDExtension sourceIDExtension) + { + processSourceIDExtension(sourceIDExtension); + } + else if(esm.isExtensionRequired()) { - if(mFrequencyBandMap.containsKey(id)) - { - channel.setFrequencyBand(mFrequencyBandMap.get(id)); - } + processSourceIDExtension(null); + mHeldTDULCMessage = tdulc; + return; } } - } - //Store band identifiers so that they can be injected into channel type messages - if(message instanceof IFrequencyBand) - { - IFrequencyBand bandIdentifier = (IFrequencyBand)message; - - //Only store the frequency band if it's new so we don't hold on to more than one instance of the - //frequency band message. Otherwise, we'll hold on to several instances of each message as they get - //injected into other messages with channel information. - if(!mFrequencyBandMap.containsKey(bandIdentifier.getIdentifier())) + //Motorola carries the talker alias in the TDULC + else if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp())) { - mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier); + dispatch(mMotorolaTalkerAliasAssembler.assemble()); } - } - } - if(mMessageListener != null) - { - if(message != null) + //Harris carries the talker alias in the LDU1 link control messages, so reset the assembler on TDULC. + mHarrisTalkerAliasAssembler.reset(); + + dispatch(tdulc); + } + else if(message instanceof HDUMessage hdu && mHeldTDULCMessage != null) { - mMessageListener.receive(message); + //If the last TDULC message was held because it needs an extension that can arrive in the first LDU1, + //hold onto the intermediate HDU message and send everything (TDU, HDU, LDU) in correct sequence. + mHeldHDUMessage = hdu; } - - if(additionalMessageToSend != null) + else { - mMessageListener.receive(additionalMessageToSend); + //flush any held messages + processSourceIDExtension(null); + dispatch(message); } } } /** - * Processes link control words to reassemble source ID extension messages. - * @param lcw to process - * @return reassembled link control or null if the original lcw is a receiver (which we hold onto and send later) + * Processes the source extension message and attaches it to any held TDULC or LDU1 with LC messages and flushes any + * held messages. + * @param extension to attach to held message(s) or null extension to flush all held messages. + */ + private void processSourceIDExtension(LCSourceIDExtension extension) + { + if(mHeldTDULCMessage != null && mHeldTDULCMessage.getLinkControlWord() instanceof IExtendedSourceMessage esm) + { + esm.setSourceIDExtension(extension); + dispatch(mHeldTDULCMessage); + mHeldTDULCMessage = null; + dispatch(mHeldHDUMessage); + mHeldHDUMessage = null; + } + + if(mHeldLDU1Message != null && mHeldLDU1Message.getLinkControlWord() instanceof IExtendedSourceMessage esm) + { + esm.setSourceIDExtension(extension); + dispatch(mHeldLDU1Message); + mHeldLDU1Message = null; + dispatch(mHeldLDU2Message); + mHeldLDU2Message = null; + } + } + + /** + * Post-process the message for frequency band details. + * @param message to post process and dispatch */ - private IMessage reassembleLC(IExtendedSourceMessage lcw) + private void dispatch(IMessage message) { - IMessage toReturn = null; + if(message == null) + { + return; + } + + processForFrequencyBands(message); - if(lcw instanceof IExtendedSourceMessage extendedSourceMessage && extendedSourceMessage.isExtensionRequired()) + //Also process the link control messages for frequency bands. + if(message instanceof LDU1Message ldu1) { - mExtendedSourceMessage = extendedSourceMessage; + processForFrequencyBands(ldu1.getLinkControlWord()); } - else if(lcw instanceof LCSourceIDExtension sie && mExtendedSourceMessage != null) + else if(message instanceof TDULCMessage tdulc) { - mExtendedSourceMessage.setSourceIDExtension(sie); - toReturn = mExtendedSourceMessage; - mExtendedSourceMessage = null; + processForFrequencyBands(tdulc.getLinkControlWord()); } - else + + if(mMessageListener != null) { - //The source extension message should always immediately follow the message that is being extended, so if - //we get a message that is not an extended message or the extension, then we've missed the extension and we - //should nullify any extended message that's waiting for the extension. - mExtendedSourceMessage = null; + mMessageListener.receive(message); } + } - return toReturn; + /** + * Captures IDEN_UPDATE messages and attaches them to messages with channel descriptors. + * @param message to process for frequency band assembly. + */ + private void processForFrequencyBands(IMessage message) + { + if(message instanceof AbstractMessage am) + { + processForFrequencyBands(am); + } + } + + /** + * Captures IDEN_UPDATE messages and attaches them to messages with channel descriptors. + * @param message to process for frequency band assembly. + */ + private void processForFrequencyBands(AbstractMessage message) + { + //Insert frequency band identifier update messages into channel-type messages */ + if(message instanceof IFrequencyBandReceiver) + { + IFrequencyBandReceiver receiver = (IFrequencyBandReceiver)message; + + List channels = receiver.getChannels(); + + for(IChannelDescriptor channel : channels) + { + int[] frequencyBandIdentifiers = channel.getFrequencyBandIdentifiers(); + + for(int id : frequencyBandIdentifiers) + { + if(mFrequencyBandMap.containsKey(id)) + { + channel.setFrequencyBand(mFrequencyBandMap.get(id)); + } + } + } + } + + //Store band identifiers so that they can be injected into channel type messages + if(message instanceof IFrequencyBand) + { + IFrequencyBand bandIdentifier = (IFrequencyBand)message; + + //Only store the frequency band if it's new so we don't hold on to more than one instance of the + //frequency band message. Otherwise, we'll hold on to several instances of each message as they get + //injected into other messages with channel information. + if(!mFrequencyBandMap.containsKey(bandIdentifier.getIdentifier())) + { + mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier); + } + } } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25MessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25MessageFactory.java index fe61a8fb4..b7eaf7789 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25MessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25MessageFactory.java @@ -23,7 +23,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU2Message; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.vselp.VSELP1Message; import io.github.dsheirer.module.decode.p25.phase1.message.vselp.VSELP2Message; @@ -65,7 +65,7 @@ public static P25P1Message create(P25P1DataUnitID dataUnitID, int nac, long time case TERMINATOR_DATA_UNIT: return new TDUMessage(message, nac, timestamp); case TERMINATOR_DATA_UNIT_LINK_CONTROL: - return new TDULinkControlMessage(message, nac, timestamp); + return new TDULCMessage(message, nac, timestamp); case TRUNKING_SIGNALING_BLOCK_1: case TRUNKING_SIGNALING_BLOCK_2: case TRUNKING_SIGNALING_BLOCK_3: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilter.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilter.java index be1df793b..ecd4901a2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilter.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilter.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,7 @@ import io.github.dsheirer.filter.FilterElement; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlOpcode; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import java.util.Collection; import java.util.function.Function; @@ -63,7 +63,7 @@ private class KeyExtractor implements Function @Override public LinkControlOpcode apply(IMessage message) { - if(message instanceof TDULinkControlMessage tdulc) + if(message instanceof TDULCMessage tdulc) { return tdulc.getLinkControlWord().getOpcode(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilterSet.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilterSet.java index bcfb51e45..e13d81229 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilterSet.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/filter/TerminatorMessageFilterSet.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlOpcode; -import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULCMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage; import java.util.ArrayList; import java.util.List; @@ -59,6 +59,6 @@ public TerminatorMessageFilterSet() @Override public boolean canProcess(IMessage message) { - return (message instanceof TDUMessage || message instanceof TDULinkControlMessage) && super.canProcess(message); + return (message instanceof TDUMessage || message instanceof TDULCMessage) && super.canProcess(message); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HeaderData.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HeaderData.java index 67f6fdbc2..4ff1eafea 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HeaderData.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/hdu/HeaderData.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more 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.p25.phase1.message.hdu; @@ -25,11 +22,11 @@ import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.module.decode.p25.identifier.encryption.APCO25EncryptionKey; import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; import io.github.dsheirer.module.decode.p25.reference.Encryption; import io.github.dsheirer.module.decode.p25.reference.Vendor; - import java.util.ArrayList; import java.util.List; @@ -49,8 +46,8 @@ public class HeaderData private boolean mValid = true; private BinaryMessage mMessage; - private Identifier mEncryptionKey; - private Identifier mTalkgroup; + private EncryptionKeyIdentifier mEncryptionKey; + private TalkgroupIdentifier mTalkgroup; private List mIdentifiers; public HeaderData(BinaryMessage message) @@ -129,9 +126,9 @@ public Vendor getVendor() return Vendor.fromValue(getMessage().getInt(VENDOR_ID)); } - public Identifier getEncryptionKey() + public EncryptionKeyIdentifier getEncryptionKey() { - if(mEncryptionKey == null && isEncryptedAudio()) + if(mEncryptionKey == null) { mEncryptionKey = EncryptionKeyIdentifier.create(APCO25EncryptionKey.create(getMessage().getInt(ALGORITHM_ID), getMessage().getInt(KEY_ID))); @@ -150,7 +147,7 @@ public boolean isEncryptedAudio() return getEncryption() != Encryption.UNENCRYPTED; } - public Identifier getTalkgroup() + public TalkgroupIdentifier getTalkgroup() { if(mTalkgroup == null) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCCallTermination.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCCallTermination.java index db9829dfc..c93e6d110 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCCallTermination.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCCallTermination.java @@ -32,7 +32,8 @@ */ public class LCCallTermination extends LinkControlWord { - private static final int MOTOROLA_SYSTEM_CONTROLLER = 0xFFFFFD; + private static final int MOTOROLA_SYSTEM_CONTROLLER_1 = 0xFFFFFD; + private static final int MOTOROLA_SYSTEM_CONTROLLER_2 = 0xFFFFFF; private static final int HARRIS_SYSTEM_CONTROLLER = 0; private static final IntField ADDRESS = IntField.length24(OCTET_6_BIT_48); private Identifier mAddress; @@ -75,7 +76,7 @@ public Identifier getAddress() public boolean isNetworkCommandedTeardown() { int address = getInt(ADDRESS); - return address == MOTOROLA_SYSTEM_CONTROLLER || address == HARRIS_SYSTEM_CONTROLLER; + return address == MOTOROLA_SYSTEM_CONTROLLER_1 || address == MOTOROLA_SYSTEM_CONTROLLER_2 || address == HARRIS_SYSTEM_CONTROLLER; } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/EncryptionSyncParameters.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/EncryptionSyncParameters.java index 5bdb9ecf0..c6428d2c1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/EncryptionSyncParameters.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/ldu/EncryptionSyncParameters.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more 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.p25.phase1.message.ldu; @@ -27,7 +24,7 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; import io.github.dsheirer.module.decode.p25.identifier.encryption.APCO25EncryptionKey; - +import io.github.dsheirer.module.decode.p25.reference.Encryption; import java.util.ArrayList; import java.util.List; @@ -36,6 +33,7 @@ */ public class EncryptionSyncParameters implements IEncryptionSyncParameters { + private static final String EMPTY_MESSAGE_INDICATOR = "000000000000000000"; private static final int[] MESSAGE_INDICATOR_1 = {0, 1, 2, 3, 4, 5, 6, 7}; private static final int[] MESSAGE_INDICATOR_2 = {8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] MESSAGE_INDICATOR_3 = {16, 17, 18, 19, 20, 21, 22, 23}; @@ -134,8 +132,16 @@ public EncryptionKeyIdentifier getEncryptionKey() { if(mEncryptionKey == null) { - mEncryptionKey = EncryptionKeyIdentifier.create(APCO25EncryptionKey.create(getMessage().getInt(ALGORITHM_ID), - getMessage().getInt(KEY_ID))); + int algorithm = getMessage().getInt(ALGORITHM_ID); + int key = getMessage().getInt(KEY_ID); + + //Detect when algorithm, key and MI are all zeros and override algorithm to set as unencrypted. + if(algorithm == 0 && key == 0 && getMessageIndicator().contains(EMPTY_MESSAGE_INDICATOR)) + { + algorithm = Encryption.UNENCRYPTED.getValue(); //0x80 + } + + mEncryptionKey = EncryptionKeyIdentifier.create(APCO25EncryptionKey.create(algorithm, key)); } return mEncryptionKey; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULCMessage.java similarity index 97% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java rename to src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULCMessage.java index ab699fed9..57bf7d8b6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULinkControlMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tdu/TDULCMessage.java @@ -31,7 +31,7 @@ import java.util.Collections; import java.util.List; -public class TDULinkControlMessage extends P25P1Message implements IFrequencyBandReceiver +public class TDULCMessage extends P25P1Message implements IFrequencyBandReceiver { public static final int[] LC_HEX_0 = {0, 1, 2, 3, 4, 5}; public static final int[] LC_HEX_1 = {6, 7, 8, 9, 10, 11}; @@ -63,7 +63,7 @@ public class TDULinkControlMessage extends P25P1Message implements IFrequencyBan private LinkControlWord mLinkControlWord; - public TDULinkControlMessage(CorrectedBinaryMessage message, int nac, long timestamp) + public TDULCMessage(CorrectedBinaryMessage message, int nac, long timestamp) { super(message, nac, timestamp); }