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 b5a1d3405..4bf91761b 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java @@ -222,7 +222,7 @@ private void load(File file) ThreadPool.CACHED.submit(() -> { List messages = new ArrayList<>(); - P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, 9600); + P25P2MessageFramer messageFramer = new P25P2MessageFramer(null); messageFramer.setScrambleParameters(scrambleParameters); P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor(); messageFramer.setListener(messageProcessor); diff --git a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java index 12eaf1088..7d710ed8e 100644 --- a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java @@ -363,6 +363,15 @@ public Identifier getToIdentifier() return to; } + /** + * Returns an encryption key identiier or null. + * @return key or null. + */ + public Identifier getEncryptionIdentifier() + { + return getIdentifier(IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY); + } + @Override public String toString() { 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 861094c50..89a113b61 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -188,10 +188,10 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch processPassport(channel, modules, aliasList, decodeConfig); break; case P25_PHASE1: - processP25Phase1(channel, userPreferences, modules, aliasList, trafficChannelManager); + processP25Phase1(channel, userPreferences, modules, aliasList, trafficChannelManager, channelDescriptor); break; case P25_PHASE2: - processP25Phase2(channel, userPreferences, modules, aliasList, trafficChannelManager); + processP25Phase2(channel, userPreferences, modules, aliasList, trafficChannelManager, channelDescriptor); break; default: throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]"); @@ -210,7 +210,8 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch * will be created automatically. */ private static void processP25Phase2(Channel channel, UserPreferences userPreferences, List modules, - AliasList aliasList, TrafficChannelManager trafficChannelManager) + AliasList aliasList, TrafficChannelManager trafficChannelManager, + IChannelDescriptor channelDescriptor) { modules.add(new P25P2DecoderHDQPSK((DecodeConfigP25Phase2)channel.getDecodeConfiguration())); @@ -236,22 +237,25 @@ else if(trafficChannelManager instanceof P25TrafficChannelManager p25) modules.add(p25TrafficChannelManager); } + //A single patch group manager is shared across both timeslots PatchGroupManager patchGroupManager = new PatchGroupManager(); - modules.add(new P25P2DecoderState(channel, P25P2Message.TIMESLOT_1, p25TrafficChannelManager, patchGroupManager)); - modules.add(new P25P2DecoderState(channel, P25P2Message.TIMESLOT_2, p25TrafficChannelManager, patchGroupManager)); + + P25P2DecoderState decoderState1 = new P25P2DecoderState(channel, P25P2Message.TIMESLOT_1, p25TrafficChannelManager, patchGroupManager); + decoderState1.setCurrentChannel(channelDescriptor); + P25P2DecoderState decoderState2 = new P25P2DecoderState(channel, P25P2Message.TIMESLOT_2, p25TrafficChannelManager, patchGroupManager); + decoderState2.setCurrentChannel(channelDescriptor); + modules.add(decoderState1); + modules.add(decoderState2); modules.add(new P25P2AudioModule(userPreferences, P25P2Message.TIMESLOT_1, aliasList)); modules.add(new P25P2AudioModule(userPreferences, P25P2Message.TIMESLOT_2, aliasList)); - //Add a channel rotation monitor when we have multiple control channel frequencies specified - if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency && - ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies()) + if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf && + sctmf.hasMultipleFrequencies()) { List activeStates = new ArrayList<>(); activeStates.add(State.CONTROL); - modules.add(new ChannelRotationMonitor(activeStates, - ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay(), - userPreferences)); + modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences)); } } @@ -263,7 +267,8 @@ else if(trafficChannelManager instanceof P25TrafficChannelManager p25) * @param aliasList for the channel */ private static void processP25Phase1(Channel channel, UserPreferences userPreferences, List modules, - AliasList aliasList, TrafficChannelManager trafficChannelManager) + AliasList aliasList, TrafficChannelManager trafficChannelManager, + IChannelDescriptor channelDescriptor) { if(channel.getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1) { @@ -288,7 +293,9 @@ private static void processP25Phase1(Channel channel, UserPreferences userPrefer } else if(trafficChannelManager instanceof P25TrafficChannelManager parentTCM) { - modules.add(new P25P1DecoderState(channel, parentTCM)); + P25P1DecoderState decoderState = new P25P1DecoderState(channel, parentTCM); + decoderState.setCurrentChannel(channelDescriptor); + modules.add(decoderState); } else { @@ -466,6 +473,7 @@ private static void processAM(Channel channel, List modules, AliasList a * @param aliasList for the audio module * @param decodeConfig for the DMR configuration * @param trafficChannelManager optional traffic channel manager to re-use + * @param channelDescriptor for the channel. */ private static void processDMR(Channel channel, UserPreferences userPreferences, List modules, AliasList aliasList, DecodeConfigDMR decodeConfig, diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java index 8686f6b63..9672b7504 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java @@ -95,7 +95,7 @@ public enum DecodeEventType /** * Voice call event types for filtering */ - public static final EnumSet VOICE_CALLS = EnumSet.of(DecodeEventType.CALL_GROUP, + public static final EnumSet VOICE_CALLS = EnumSet.of(DecodeEventType.CALL, DecodeEventType.CALL_GROUP, DecodeEventType.CALL_PATCH_GROUP, DecodeEventType.CALL_ALERT, DecodeEventType.CALL_DETECT, DecodeEventType.CALL_DO_NOT_MONITOR, DecodeEventType.CALL_END, DecodeEventType.CALL_INTERCONNECT, DecodeEventType.CALL_UNIQUE_ID, DecodeEventType.CALL_UNIT_TO_UNIT, DecodeEventType.CALL_NO_TUNER, diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25ChannelGrantEvent.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25ChannelGrantEvent.java index 0e4e694a3..688256a72 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25ChannelGrantEvent.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25ChannelGrantEvent.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 @@ -80,6 +80,7 @@ public static class P25ChannelGrantDecodeEventBuilder protected IChannelDescriptor mChannelDescriptor; protected String mDetails; private ServiceOptions mServiceOptions; + private int mTimeslot = -1; /** * Constructs a builder instance with the specified start time in milliseconds @@ -144,6 +145,16 @@ public P25ChannelGrantDecodeEventBuilder details(String details) return this; } + /** + * Sets the timeslot for the event + * @param timeslot + */ + public P25ChannelGrantDecodeEventBuilder timeslot(int timeslot) + { + mTimeslot = timeslot; + return this; + } + /** * Builds the decode event */ @@ -156,6 +167,7 @@ public P25ChannelGrantEvent build() decodeEvent.setDuration(mDuration); decodeEvent.setIdentifierCollection(mIdentifierCollection); decodeEvent.setServiceOptions(mServiceOptions); + decodeEvent.setTimeslot(mTimeslot); return decodeEvent; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java new file mode 100644 index 000000000..beedd105d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java @@ -0,0 +1,284 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25; + +import io.github.dsheirer.channel.IChannelDescriptor; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.MutableIdentifierCollection; +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. + */ +public class P25TrafficChannelEventTracker +{ + private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class); + private static final long STALE_EVENT_THRESHOLD_MS = 2000; + private P25ChannelGrantEvent mEvent; + private boolean mStarted = false; + private boolean mComplete = false; + + /** + * Constructs an instance + * @param event to track for the traffic channel. + */ + public P25TrafficChannelEventTracker(P25ChannelGrantEvent event) + { + mEvent = event; + } + + /** + * Access the underlying traffic channel event that is being tracked. + * @return event. + */ + public P25ChannelGrantEvent getEvent() + { + return mEvent; + } + + /** + * 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. + */ + public boolean isStale(long timestamp) + { + if(getEvent().getTimeEnd() > 0) + { + return timestamp - getEvent().getTimeEnd() > STALE_EVENT_THRESHOLD_MS; + } + + return timestamp - getEvent().getTimeStart() > STALE_EVENT_THRESHOLD_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()); + mic.update(identifier); + getEvent().setIdentifierCollection(mic); + } + } + + /** + * 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 + */ + public void addDetailsIfMissing(String additionalDetails) + { + if(additionalDetails != null && !additionalDetails.isEmpty()) + { + if(getEvent().getDetails() == null) + { + getEvent().setDetails(additionalDetails); + } + else if(!getEvent().getDetails().endsWith(additionalDetails)) + { + getEvent().setDetails(getEvent().getDetails() + " " + additionalDetails); + } + } + } + + /** + * Compares the TO role identifier(s) from the tracked event and the identifier collection argument for equality + * and also checks this event for staleness. + * + * @param toCompare containing a TO identifier + * @param timestamp to check for staleness + * @return true if both collections contain a TO identifier and the TO identifiers are the same value + */ + public boolean isSameCallCheckingToOnly(IdentifierCollection toCompare, long timestamp) + { + Identifier currentTO = getEvent().getIdentifierCollection().getToIdentifier(); + Identifier nextTO = toCompare.getToIdentifier(); + return currentTO != null && currentTO.equals(nextTO) && !isStale(timestamp); + } + + /** + * 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. + */ + public boolean isDifferentTalker(Identifier fromToCompare) + { + if(fromToCompare == null) + { + return false; + } + + Identifier fromCurrent = getEvent().getIdentifierCollection().getFromIdentifier(); + return fromCurrent != null && !fromCurrent.equals(fromToCompare); + } + + + /** + * 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. + */ + public boolean isSameCallCheckingToAndFrom(IdentifierCollection toCompareIC, long timestamp) + { + if(!isStale(timestamp)) + { + Identifier currentTO = getEvent().getIdentifierCollection().getToIdentifier(); + Identifier nextTO = toCompareIC.getToIdentifier(); + + if(currentTO != null && currentTO.equals(nextTO)) + { + Identifier existingFROM = getEvent().getIdentifierCollection().getFromIdentifier(); + + //If the FROM identifier hasn't yet been established, then this is the same call. We also ignore the + //talker alias as a call identifier since on L3Harris systems they can transmit the talker alias before + //they transmit the radio ID. + if(existingFROM == null || existingFROM.getForm() == Form.TALKER_ALIAS) + { + return true; + } + + Identifier nextFROM = toCompareIC.getFromIdentifier(); + + //Sometime the GROUP_VOICE_CHANNEL_USER has a zero valued FROM address which is not valid, so if the + //nextFROM is null, we consider the call to be the same. Likewise, if the nextFROM is a talker alias, + //that's also the same call. + if(nextFROM == null || (nextFROM != null && nextFROM.getForm() == Form.TALKER_ALIAS)) + { + return true; + } + + return existingFROM.equals(nextFROM); + } + } + + return false; + } + + /** + * Indicates if the event has been marked as complete by traffic channel signalling. + * @return complete status. + */ + public boolean isComplete() + { + return mComplete; + } + + /** + * Indicates if the event has been marked as started by the traffic channel for HDU or LDU signalling. + * @return started status. + */ + public boolean isStarted() + { + return mStarted; + } + + /** + * Updates the event duration using signalling from the control channel. + * + * Note: once the traffic channel starts updating the event timing, attempts to update timing from the control + * channel are ignored. + * + * @param timestamp to use as the current end timestamp for the event. + * @return true if the timestamp was updated. + */ + public boolean updateDurationControl(long timestamp) + { + if(!isStarted()) + { + getEvent().update(timestamp); + return true; + } + + return false; + } + + /** + * Updates the event duration using signalling from the traffic channel. + * + * 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) + { + if(!isComplete()) + { + mStarted = true; + getEvent().update(timestamp); + } + else + { + LOGGER.warn("Attempt to update event call event duration from traffic channel against an event that is marked as complete"); + } + } + + /** + * 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 + */ + public boolean completeTraffic(long timestamp) + { + if(!isComplete()) + { + mComplete = true; + getEvent().end(timestamp); + return true; + } + + return false; + } + + /** + * Updates the details for the tracked event. + * @param details to update + */ + public void setDetails(String details) + { + getEvent().setDetails(details); + } + + /** + * Updates the channel descriptor for the tracked event. + * @param channelDescriptor to update. + */ + public void addChannelDescriptorIfMissing(IChannelDescriptor channelDescriptor) + { + if(channelDescriptor != null && getEvent().getChannelDescriptor() == null) + { + getEvent().setChannelDescriptor(channelDescriptor); + } + } +} 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 24d3f0038..905133b09 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 @@ -26,7 +26,6 @@ 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.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection; @@ -48,7 +47,6 @@ import io.github.dsheirer.module.decode.p25.identifier.channel.P25Channel; import io.github.dsheirer.module.decode.p25.identifier.channel.P25P2Channel; import io.github.dsheirer.module.decode.p25.identifier.channel.P25P2ExplicitChannel; -import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel; import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; @@ -75,21 +73,27 @@ import org.slf4j.LoggerFactory; /** - * Monitors channel grant and channel grant update messages to allocate traffic channels to capture - * traffic channel activity. + * Monitors traffic channel state for a control channel and monitors individual traffic channel updates to track all + * call events across the system. * - * Creates and reuses a limited set of Channel instances each with a TRAFFIC channel type. Since each of - * the traffic channels will be using the same decoder type and configuration options, we reuse each of the - * channel instances to allow the ChannelProcessingManager to reuse a cached set of processing chains that - * are created as each channel is activated. + * Call events originate on the control channel and receive continuing updates from the control channel. Events + * continue on the traffic channel. The call event trackers manage the call event until call end where the call + * duration is updated from the control messaging and once the traffic channel is allocated the tracker only accepts + * event timing updates from the traffic messaging and at the end of the call the tracker marks the event as complete. + * Further updates to an ended event will cause the event to be removed and replaced with the next tracked event. * - * Each traffic channel is activated by sending a ChannelEvent via the channel model. The channel processing - * manager receives the activation request. If successful, a processing chain is activated for the traffic - * channel. Otherwise, a channel event is broadcast indicating that the channel could not be activated. On - * teardown of an activated traffic channel, a channel event is broadcast to indicate the traffic channels - * is no longer active. + * Creates and reuses a limited set of Channel instances each with a TRAFFIC channel type. Since each of the traffic + * channels will be using the same decoder type and configuration options, we reuse each of the channel instances to + * allow the ChannelProcessingManager to reuse a cached set of processing chains that are created as each channel is + * activated. * - * This manager monitors channel events watching for events related to managed traffic channels. + * Each traffic channel is activated by sending a ChannelEvent via the channel model. The channel processing manager + * receives the activation request. If successful, a processing chain is activated for the traffic channel. Otherwise, + * a channel event is broadcast indicating that the channel could not be activated. On teardown of an activated traffic + * channel, a channel event is broadcast to indicate the traffic channels is no longer active. + * + * The ReentrantLock (mLock) protects threaded access to the mTSxChannelGrantEventMaps and to the allocated and + * available traffic channel lists and queues. */ public class P25TrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener, IChannelEventProvider, IMessageListener @@ -98,15 +102,15 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog); public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED"; - private static final long STALE_EVENT_THRESHOLD_MS = 2000; private Queue mAvailablePhase1TrafficChannelQueue = new LinkedTransferQueue<>(); - private List mManagedPhase1TrafficChannels; private Queue mAvailablePhase2TrafficChannelQueue = new LinkedTransferQueue<>(); + private List mManagedPhase1TrafficChannels; private List mManagedPhase2TrafficChannels; private Map mAllocatedTrafficChannelMap = new HashMap<>(); - private Map mTS1ChannelGrantEventMap = new HashMap<>(); - private Map mTS2ChannelGrantEventMap = new HashMap<>(); + private Map mTS1ChannelGrantEventMap = new HashMap<>(); + private Map mTS2ChannelGrantEventMap = new HashMap<>(); + private ReentrantLock mLock = new ReentrantLock(); private Map mFrequencyBandMap = new ConcurrentHashMap<>(); private Listener mChannelEventListener; private Listener mDecodeEventListener; @@ -115,10 +119,8 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I private ScrambleParameters mPhase2ScrambleParameters; private Listener mMessageListener; private boolean mIgnoreDataCalls; - private boolean mIdleNullProtect = false; //Used only for data calls private DecodeEventDuplicateDetector mDuplicateDetector = new DecodeEventDuplicateDetector(); - private ReentrantLock mLock = new ReentrantLock(); private TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager(); /** @@ -182,19 +184,21 @@ protected void processControlFrequencyUpdate(long previous, long current, Channe { //Shutdown all existing traffic channels and clear the maps. List trafficChannelsToDisable = new ArrayList<>(mAllocatedTrafficChannelMap.values()); - mAllocatedTrafficChannelMap.clear(); - for(Channel channel : trafficChannelsToDisable) + for(Channel channelToDisable : trafficChannelsToDisable) { - if(!parentChannel.equals(channel)) + if(!parentChannel.equals(channelToDisable)) { - broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); + broadcast(new ChannelEvent(channelToDisable, Event.REQUEST_DISABLE)); } } mTS1ChannelGrantEventMap.clear(); mTS2ChannelGrantEventMap.clear(); + //Remove the control channel from the previous frequency + mAllocatedTrafficChannelMap.remove(previous); + //Store the current control channel in the allocated channel map so that we don't allocate a traffic channel against it mAllocatedTrafficChannelMap.put(current, parentChannel); } @@ -283,6 +287,100 @@ public void broadcast(DecodeEvent decodeEvent) } } + /** + * Broadcasts the decode event from the tracker. + * @param tracker containing a decode event. + */ + public void broadcast(P25TrafficChannelEventTracker tracker) + { + broadcast(tracker.getEvent()); + } + + /** + * 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 channel to look up the tracker + * @param timestamp to compare for staleness + * @return tracker or null + */ + private P25TrafficChannelEventTracker getTrackerRemoveIfStale(APCO25Channel channel, long timestamp) + { + return getTrackerRemoveIfStale(channel.getDownlinkFrequency(), channel.getTimeslot(), timestamp); + } + + /** + * 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 timeslot to look up the tracker + * @param timestamp to compare for staleness + * @return tracker or null + */ + private P25TrafficChannelEventTracker getTrackerRemoveIfStale(long frequency, int timeslot, long timestamp) + { + P25TrafficChannelEventTracker tracker = getTracker(frequency, timeslot); + + if(tracker != null && tracker.isStale(timestamp)) + { + removeTracker(frequency, timeslot); + tracker = null; + } + + return tracker; + } + + /** + * Gets the tracker from the correct channel grant map. + * @param frequency for the map lookup + * @param timeslot to identify the correct map. + */ + private P25TrafficChannelEventTracker getTracker(long frequency, int timeslot) + { + if(timeslot == P25P1Message.TIMESLOT_2) + { + return mTS2ChannelGrantEventMap.get(frequency); + } + else + { + return mTS1ChannelGrantEventMap.get(frequency); + } + } + + /** + * Adds the tracker to the correct channel grant map. + * @param tracker to add + * @param frequency for the map lookup + * @param timeslot to identify the correct map. + */ + private void addTracker(P25TrafficChannelEventTracker tracker, long frequency, int timeslot) + { + if(timeslot == P25P1Message.TIMESLOT_2) + { + mTS2ChannelGrantEventMap.put(frequency, tracker); + } + else + { + mTS1ChannelGrantEventMap.put(frequency, tracker); + } + } + + /** + * Removes the tracker from the correct channel grant map. + * @param frequency for the map lookup + * @param timeslot to identify the correct map. + */ + private void removeTracker(long frequency, int timeslot) + { + if(timeslot == P25P1Message.TIMESLOT_2) + { + mTS2ChannelGrantEventMap.remove(frequency); + } + else + { + mTS1ChannelGrantEventMap.remove(frequency); + } + } + /** * Processes a P25 Phase 2 channel update for any channel. If the initial channel grant was not detected, invokes * the process channel grant method to auto-create the channel. @@ -297,7 +395,7 @@ public void broadcast(DecodeEvent decodeEvent) * @param timestamp of the message */ public void processP2ChannelUpdate(APCO25Channel channel, ServiceOptions serviceOptions, - IdentifierCollection ic, MacOpcode macOpcode, long timestamp) + IdentifierCollection ic, MacOpcode macOpcode, long timestamp, String context) { if(channel.getDownlinkFrequency() > 0) { @@ -309,7 +407,7 @@ public void processP2ChannelUpdate(APCO25Channel channel, ServiceOptions service if(!processing) { - processP2ChannelGrant(channel, serviceOptions, ic, macOpcode, timestamp); + processP2ChannelGrant(channel, serviceOptions, ic, macOpcode, timestamp, context); } } finally @@ -323,95 +421,135 @@ public void processP2ChannelUpdate(APCO25Channel channel, ServiceOptions service * Closes the call event for the specified channel frequency and timeslot. * @param frequency for the channel * @param timeslot for the channel. - * @param isIdleNull to indicate if this close event is trigged by an IDLE/NULL message + * @param timestamp for the final duration update for the event + * @return true if the current tracked call event was ended */ - public void closeP2CallEvent(long frequency, int timeslot, boolean isIdleNull) + public boolean processP2TrafficCallEnd(long frequency, int timeslot, long timestamp, String context) { - /** - * Hack: L3Harris systems can issue a channel grant on control/TS1 which creates an event for TS2 and then the - * next message for TS2 is an IDLE/NULL which causes the event that was just created to be closed. So, we set - * a protect flag on the channel grant and then ignore the first IDLE/NULL triggered close event call, to - * protect the just created event. - */ - if(isIdleNull && mIdleNullProtect) - { - mIdleNullProtect = false; - return; - } + boolean completed = false; mLock.lock(); try { - if(timeslot == P25P1Message.TIMESLOT_0 || timeslot == P25P1Message.TIMESLOT_1) + P25TrafficChannelEventTracker tracker = getTracker(frequency, timeslot); + + //If we have a tracker that we can mark complete, broadcast the updated tracker/event. + if(tracker != null && tracker.completeTraffic(timestamp)) { - mTS1ChannelGrantEventMap.remove(frequency); + completed = true; + broadcast(tracker); } - else + } + finally + { + mLock.unlock(); + } + + return completed; + } + + /** + * Processes a traffic channel end push to talk event. The PTT event is handled separately from the call end + * method because there can be a timing issue between when the control channel issues the channel grant event and + * when the traffic channel closes out traffic on the same channel, which can cause an event to be closed + * prematurely. This method checks to see if the call has been started before closing it out. + * @param frequency for the channel + * @param timeslot for the channel. + * @param timestamp for the final duration update for the event + * @return true if the current tracked call event was ended + */ + public boolean processP2TrafficEndPushToTalk(long frequency, int timeslot, long timestamp, String context) + { + boolean completed = false; + + mLock.lock(); + + try + { + P25TrafficChannelEventTracker tracker = getTracker(frequency, timeslot); + + //If we have a tracker that is started that we can mark complete, broadcast the updated tracker/event. + if(tracker != null && tracker.isStarted() && tracker.completeTraffic(timestamp)) { - mTS2ChannelGrantEventMap.remove(frequency); + completed = true; + broadcast(tracker); } } finally { mLock.unlock(); } + + return completed; } /** - * Updates an identifier for an ongoing call event on the frequency and timeslot. + * Updates an identifier and call duration for an ongoing call event on the frequency and timeslot. * * Note: if this manager does not have an existing call event for the frequency and timeslot, the update is ignored * because we don't have enough detail to create a call event. * - * This is used primarily to add encryption, GPS, or a talker alias, but can be used for any identifier update. + * This is used primarily to add encryption, GPS, talker alias, etc. but can be used for any identifier update + * where the update infers that a call event is ongoing. * * @param frequency for the call event * @param timeslot for the call event * @param identifier to update within the event. * @param timestamp for the update */ - public void processP2CurrentUser(long frequency, int timeslot, Identifier identifier, long timestamp) + public void processP2TrafficCurrentUser(long frequency, int timeslot, Identifier identifier, long timestamp) { mLock.lock(); try { - DecodeEvent event = null; + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timeslot, timestamp); - if(timeslot == P25P1Message.TIMESLOT_1) + if(tracker != null && tracker.isComplete()) { - event = mTS1ChannelGrantEventMap.get(frequency); - - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS1ChannelGrantEventMap.remove(frequency); - } + removeTracker(frequency, timeslot); + tracker = null; } - else if(timeslot == P25P1Message.TIMESLOT_2) - { - event = mTS2ChannelGrantEventMap.get(frequency); - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS1ChannelGrantEventMap.remove(frequency); - } + if(tracker != null) + { + tracker.addIdentifierIfMissing(identifier); + tracker.updateDurationTraffic(timestamp); + broadcast(tracker); } + } + finally + { + mLock.unlock(); + } + } + + /** + * Starts a tracked event and updates the duration for a tracked event. + * + * @param frequency for the call event + * @param timeslot for the call event + * @param timestamp for the update + */ + public void processP2TrafficVoice(long frequency, int timeslot, long timestamp) + { + mLock.lock(); - if(event != null) + try + { + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timeslot, timestamp); + + if(tracker != null && tracker.isComplete()) { - if(!event.getIdentifierCollection().hasIdentifier(identifier)) - { - MutableIdentifierCollection mic = new MutableIdentifierCollection(event.getIdentifierCollection() - .getIdentifiers()); - mic.update(identifier); - event.setIdentifierCollection(mic); - } + removeTracker(frequency, timeslot); + tracker = null; + } - event.update(timestamp); - broadcast(event); + if(tracker != null) + { + tracker.updateDurationTraffic(timestamp); + broadcast(tracker); } } finally @@ -420,10 +558,10 @@ else if(timeslot == P25P1Message.TIMESLOT_2) } } - - /** - * Processes a call on the current channel + * Processes mac messaging that indicates a call on the current channel. This update does not update the call + * duration for the event but will create an event if one does not exist. Updates to the call duration can only + * occur * @param frequency of the current channel * @param timeslot of the current channel * @param serviceOptions for the call @@ -431,86 +569,33 @@ else if(timeslot == P25P1Message.TIMESLOT_2) * @param timestamp for the message that is being processed * @return */ - public IChannelDescriptor processP2CurrentUser(long frequency, int timeslot, IChannelDescriptor channelDescriptor, - ServiceOptions serviceOptions, MacOpcode macOpcode, - IdentifierCollection ic, long timestamp, String additionalDetails) + public IChannelDescriptor processP2TrafficCurrentUser(long frequency, int timeslot, IChannelDescriptor channelDescriptor, + ServiceOptions serviceOptions, MacOpcode macOpcode, + IdentifierCollection ic, long timestamp, String additionalDetails, String context) { mLock.lock(); try { - DecodeEvent event = null; + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timeslot, timestamp); - if(timeslot == P25P1Message.TIMESLOT_1) + if(tracker != null && tracker.isComplete()) { - event = mTS1ChannelGrantEventMap.get(frequency); - - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS1ChannelGrantEventMap.remove(frequency); - } - } - else if(timeslot == P25P1Message.TIMESLOT_2) - { - event = mTS2ChannelGrantEventMap.get(frequency); - - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS2ChannelGrantEventMap.remove(frequency); - } + removeTracker(frequency, timeslot); + tracker = null; } - if(event != null) + if(tracker != null && tracker.isSameCallCheckingToAndFrom(ic, timestamp)) { - if(isSameCallFull(event.getIdentifierCollection(), ic)) + for(Identifier identifier: ic.getIdentifiers()) { - boolean updateRequired = false; - - for(Identifier identifier: ic.getIdentifiers()) - { - if(!event.getIdentifierCollection().hasIdentifier(identifier)) - { - updateRequired = true; - break; - } - } - - if(updateRequired) - { - MutableIdentifierCollection mic = new MutableIdentifierCollection(event.getIdentifierCollection().getIdentifiers()); - for(Identifier identifier: ic.getIdentifiers()) - { - mic.update(identifier); - } - - event.setIdentifierCollection(mic); - } - - event.update(timestamp); - event.setDecodeEventType(getEventType(macOpcode, serviceOptions, event.getEventType())); - - if(additionalDetails != null) - { - if(event.getDetails() == null) - { - event.setDetails(additionalDetails); - } - else if(!event.getDetails().endsWith(additionalDetails)) - { - event.setDetails(event.getDetails() + " " + additionalDetails); - } - } - - if(event.getChannelDescriptor() == null) - { - event.setChannelDescriptor(channelDescriptor); - } - - broadcast(event); - return event.getChannelDescriptor(); + tracker.addIdentifierIfMissing(identifier); } + + tracker.addDetailsIfMissing(additionalDetails); + tracker.addChannelDescriptorIfMissing(channelDescriptor); + broadcast(tracker); + return tracker.getEvent().getChannelDescriptor(); } //Create a new event for the current call. @@ -522,16 +607,9 @@ else if(!event.getDetails().endsWith(additionalDetails)) .identifiers(ic) .build(); - if(timeslot == P25P1Message.TIMESLOT_0 || timeslot == P25P1Message.TIMESLOT_1) - { - mTS1ChannelGrantEventMap.put(frequency, callEvent); - } - else - { - mTS2ChannelGrantEventMap.put(frequency, callEvent); - } - - broadcast(callEvent); + tracker = new P25TrafficChannelEventTracker(callEvent); + addTracker(tracker, frequency, timeslot); + broadcast(tracker); return null; } finally @@ -550,7 +628,7 @@ else if(!event.getDetails().endsWith(additionalDetails)) * @param macOpcode to identify the call type for the event description */ public void processP2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, - IdentifierCollection ic, MacOpcode macOpcode, long timestamp) + IdentifierCollection ic, MacOpcode macOpcode, long timestamp, String context) { mLock.lock(); @@ -567,17 +645,13 @@ public void processP2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions se if(macOpcode.isDataChannelGrant()) { APCO25Channel phase1Channel = convertPhase2ToPhase1(apco25Channel); - processPhase1ChannelGrant(phase1Channel, serviceOptions, ic, decodeEventType, - isDataChannelGrant, timestamp); + processPhase1ControlChannelGrant(phase1Channel, serviceOptions, ic, decodeEventType, + isDataChannelGrant, timestamp, context); } else { processPhase2ChannelGrant(apco25Channel, serviceOptions, ic, decodeEventType, isDataChannelGrant, timestamp); - - //Hack: set the IDLE/NULL protect flag so that this event doesn't get immediately closed on - //L3Harris control channels. - mIdleNullProtect = true; } } else @@ -588,8 +662,8 @@ public void processP2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions se } else { - processPhase1ChannelGrant(apco25Channel, serviceOptions, ic, decodeEventType, isDataChannelGrant, - timestamp); + processPhase1ControlChannelGrant(apco25Channel, serviceOptions, ic, decodeEventType, isDataChannelGrant, + timestamp, context); } } @@ -599,17 +673,17 @@ public void processP2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions se } } - /** - * Processes phase 1 channel grants to allocate traffic channels and track overall channel usage. Generates and - * tracks decode events for each new channel that is allocated. - * - * @param apco25Channel for the traffic channel - * @param serviceOptions for the traffic channel - optional can be null - * @param identifierCollection associated with the channel grant - * @param opcode to identify the call type for the event description - */ - public void processP1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, - IdentifierCollection identifierCollection, Opcode opcode, long timestamp) + /** + * Processes phase 1 channel grants directed by the control channel to allocate traffic channels and track overall + * channel usage. Generates and tracks decode events for each new channel that is allocated. + * + * @param apco25Channel for the granted traffic channel + * @param serviceOptions for the granted traffic channel - optional can be null + * @param ic associated with the channel grant + * @param opcode to identify the call type for the event description + */ + public void processP1ControlDirectedChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, + IdentifierCollection ic, Opcode opcode, long timestamp, String context) { mLock.lock(); @@ -620,13 +694,13 @@ public void processP1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions se if(apco25Channel.isTDMAChannel()) { - processPhase2ChannelGrant(apco25Channel, serviceOptions, identifierCollection, decodeEventType, + processPhase2ChannelGrant(apco25Channel, serviceOptions, ic, decodeEventType, isDataChannelGrant, timestamp); } else { - processPhase1ChannelGrant(apco25Channel, serviceOptions, identifierCollection, decodeEventType, - isDataChannelGrant, timestamp); + processPhase1ControlChannelGrant(apco25Channel, serviceOptions, ic, decodeEventType, + isDataChannelGrant, timestamp, context); } } finally @@ -636,50 +710,43 @@ public void processP1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions se } /** - * Updates an identifier for an ongoing call event on the frequency. + * Updates an identifier 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, or a talker alias, but can be used for any identifier update. + * 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 processP1CurrentUser(long frequency, Identifier identifier, long timestamp) + public void processP1TrafficCurrentUser(long frequency, Identifier identifier, long timestamp, String context) { mLock.lock(); try { - DecodeEvent event = mTS1ChannelGrantEventMap.get(frequency); + P25TrafficChannelEventTracker tracker = getTracker(frequency, P25P1Message.TIMESLOT_1); - if(event != null) + if(tracker != null && tracker.isComplete()) { - if(!event.getIdentifierCollection().hasIdentifier(identifier)) - { - MutableIdentifierCollection mic = new MutableIdentifierCollection(event.getIdentifierCollection() - .getIdentifiers()); - mic.update(identifier); - event.setIdentifierCollection(mic); - } + removeTracker(frequency, P25P1Message.TIMESLOT_1); + tracker = null; + } + + if(tracker != null) + { + tracker.addIdentifierIfMissing(identifier); //Add the encryption key to the call event details. if(identifier instanceof EncryptionKeyIdentifier eki && eki.isEncrypted()) { - if(event.getDetails() == null) - { - event.setDetails(eki.toString()); - } - else if(!event.getDetails().contains(eki.toString())) - { - event.setDetails(event.getDetails() + " " + eki); - } + tracker.addDetailsIfMissing(eki.toString()); } - event.update(timestamp); - broadcast(event); + tracker.updateDurationTraffic(timestamp); + broadcast(tracker); } } finally @@ -689,73 +756,42 @@ else if(!event.getDetails().contains(eki.toString())) } /** - * Processes a call on the current channel - * @param frequency of the current channel - * @param channelDescriptor for the current channel - * @param decodeEventType to use for the event if it doesn't already exist + * Processes traffic channel announced current user information. + * @param frequency of the traffic channel + * @param channelDescriptor for the traffic channel + * @param decodeEventType to use if the call event is not currently tracked * @param serviceOptions for the call * @param ic identifiers for the call * @param timestamp for the message that is being processed * @return channel descriptor for the event or null */ - public IChannelDescriptor processP1CurrentUser(long frequency, IChannelDescriptor channelDescriptor, - DecodeEventType decodeEventType, ServiceOptions serviceOptions, - IdentifierCollection ic, long timestamp, String additionalDetails) + public void processP1TrafficCurrentUser(long frequency, IChannelDescriptor channelDescriptor, + DecodeEventType decodeEventType, ServiceOptions serviceOptions, + IdentifierCollection ic, long timestamp, String additionalDetails, String context) { mLock.lock(); try { - DecodeEvent event = mTS1ChannelGrantEventMap.get(frequency); + P25TrafficChannelEventTracker tracker = getTracker(frequency, P25P1Message.TIMESLOT_1); - if(event != null) + if(tracker != null && tracker.isComplete()) { - if(isSameCallFull(event.getIdentifierCollection(), ic)) - { - boolean updateRequired = false; - - for(Identifier identifier: ic.getIdentifiers()) - { - if(!event.getIdentifierCollection().hasIdentifier(identifier)) - { - updateRequired = true; - break; - } - } - - if(updateRequired) - { - MutableIdentifierCollection mic = new MutableIdentifierCollection(event.getIdentifierCollection().getIdentifiers()); - for(Identifier identifier: ic.getIdentifiers()) - { - mic.update(identifier); - } - - event.setIdentifierCollection(mic); - } - - event.update(timestamp); - - if(additionalDetails != null) - { - if(event.getDetails() == null) - { - event.setDetails(additionalDetails); - } - else if(!event.getDetails().endsWith(additionalDetails)) - { - event.setDetails(event.getDetails() + " " + additionalDetails); - } - } - - broadcast(event); - return event.getChannelDescriptor(); - } + removeTracker(frequency, P25P1Message.TIMESLOT_1); + tracker = null; } - if(channelDescriptor == null && frequency > 0) + if(tracker != null && tracker.isSameCallCheckingToAndFrom(ic, timestamp)) { - channelDescriptor = new StandardChannel(frequency); + for(Identifier identifier: ic.getIdentifiers()) + { + tracker.addIdentifierIfMissing(identifier); + } + + tracker.updateDurationTraffic(timestamp); + tracker.addDetailsIfMissing(additionalDetails); + broadcast(tracker); + return; } //Create a new event for the current call. @@ -766,9 +802,9 @@ else if(!event.getDetails().endsWith(additionalDetails)) .identifiers(ic) .build(); - mTS1ChannelGrantEventMap.put(frequency, callEvent); - broadcast(callEvent); - return null; + tracker = new P25TrafficChannelEventTracker(callEvent); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); + broadcast(tracker); } finally { @@ -777,11 +813,12 @@ else if(!event.getDetails().endsWith(additionalDetails)) } /** - * Processes a P25 Phase 1 channel update for any channel. If the initial channel grant was not detected, invokes - * the process channel grant method to auto-create the channel. + * Processes a P25 Phase 1 control channel announced update to an allocated traffic channel. If the initial traffic + * channel grant was not detected, invokes the process channel grant method to auto-create the channel. * - * For the update message, we normally only get the TO talkgroup value, so we'll do a comparison of the event using - * just the TO identifier. + * From the update message we only get the TO talkgroup value, so we'll do a tracked event comparison using only + * the TO identifier to determine if the update refers to the currently tracked event or if the update is for a + * new and different event. * * @param channel where the activity is taking place. * @param serviceOptions for the call @@ -789,46 +826,31 @@ else if(!event.getDetails().endsWith(additionalDetails)) * @param opcode for the update message * @param timestamp of the message */ - public void processP1ChannelUpdate(APCO25Channel channel, ServiceOptions serviceOptions, - IdentifierCollection ic, Opcode opcode, long timestamp) + public void processP1ControlAnnouncedTrafficUpdate(APCO25Channel channel, ServiceOptions serviceOptions, + IdentifierCollection ic, Opcode opcode, long timestamp, String context) { - mLock.lock(); try { - DecodeEvent event = null; + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(channel, timestamp); - if(channel.isTDMAChannel() && channel.getTimeslot() == P25P1Message.TIMESLOT_2) + //If we have a tracked event, update it. Otherwise, make sure we have the traffic channel allocated + if(tracker != null && tracker.isSameCallCheckingToOnly(ic, timestamp)) { - event = mTS2ChannelGrantEventMap.get(channel.getDownlinkFrequency()); - // If the event is stale, remove it and allow a new event to be created. - if(isStaleEvent(event, timestamp)) + //Only rebroadcast the tracked event if the timestamp was updated from this control channel timestamp + //Once the traffic channel takes over updating the tracked event time/duration, further control channel + // updates are ignored. + if(tracker.updateDurationControl(timestamp)) { - event = null; - mTS2ChannelGrantEventMap.remove(channel.getDownlinkFrequency()); + broadcast(tracker); } } else { - event = mTS1ChannelGrantEventMap.get(channel.getDownlinkFrequency()); - // If the event is stale, remove it and allow a new event to be created. - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS1ChannelGrantEventMap.remove(channel.getDownlinkFrequency()); - } - } - - //If we have an event, update it. Otherwise, make sure we have the traffic channel allocated - if(event != null && isSameCallUpdate(event.getIdentifierCollection(), ic)) - { - event.update(timestamp); - broadcast(event); - } - else - { - processP1ChannelGrant(channel, serviceOptions, ic, opcode, timestamp); + //Remove the existing call tracker because it's a different call now + removeTracker(channel.getDownlinkFrequency(), channel.getTimeslot()); + processP1ControlDirectedChannelGrant(channel, serviceOptions, ic, opcode, timestamp, context); } } finally @@ -838,102 +860,121 @@ public void processP1ChannelUpdate(APCO25Channel channel, ServiceOptions service } /** - * Closes the call event for the specified channel frequency. + * Marks the tracked traffic channel event as complete but does not remove the event from the tracker map so + * that we can continue to track the event for the traffic channel. * @param frequency for the channel + * @return true if the tracked call event was marked as complete. */ - public void closeP1CallEvent(long frequency, long timestamp) + public boolean processP1TrafficCallEnd(long frequency, long timestamp, String context) { + boolean completed = false; + mLock.lock(); try { - DecodeEvent event = mTS1ChannelGrantEventMap.remove(frequency); + P25TrafficChannelEventTracker tracker = getTracker(frequency, P25P1Message.TIMESLOT_1); - if(event != null) + //If we have a tracker that we can mark complete, broadcast the updated tracker/event. + if(tracker != null && tracker.isStarted() && tracker.completeTraffic(timestamp)) { - event.end(timestamp); - broadcast(event); + completed = true; + broadcast(tracker); } } finally { mLock.unlock(); } + + return completed; } /** - * Indicates if the event is stale and should be replaced with a new decode event. Compares the event down time - * to the reference timestamp argument and indicates if that staleness exceeds a staleness threshold (2 seconds). + * Sends a channel start request to the ChannelProcessingManager. * - * Note: some decode events can remain in the channel grant event map if a traffic channel is not allocated for the - * event (ie tuner unavailable). The control channel only tells us that the traffic channel is active, but doesn't - * tell us when the traffic channel is torn down. So, we use this staleness evaluation to remove stale events that - * are left over from a previous traffic channel allocation. + * Note: this method is not thread safe and the calling method must protect access using mLock. * - * @param decodeEvent to evaluate - * @param timestamp for reference of event staleness. - * @return true if the event is stale. + * @param trafficChannel to use for the traffic channel + * @param apco25Channel that describes the traffic channel downlink frequency + * @param identifierCollection containing identifiers for the call + * @param timestamp of the request event */ - private static boolean isStaleEvent(DecodeEvent decodeEvent, long timestamp) + private void requestTrafficChannelStart(Channel trafficChannel, APCO25Channel apco25Channel, + IdentifierCollection identifierCollection, long timestamp) { - return decodeEvent != null && (timestamp - decodeEvent.getTimeEnd() > STALE_EVENT_THRESHOLD_MS); + if(getInterModuleEventBus() != null) + { + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency()); + if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) + { + sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); + } + trafficChannel.setSourceConfiguration(sourceConfig); + + //If this is a phase2 request and we have scramble/randomizer parameters, set them + if(mPhase2ScrambleParameters != null && trafficChannel.getDecodeConfiguration() instanceof DecodeConfigP25Phase2 p2) + { + p2.setScrambleParameters(mPhase2ScrambleParameters.copy()); + } + + mAllocatedTrafficChannelMap.put(apco25Channel.getDownlinkFrequency(), trafficChannel); + + ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel, + apco25Channel, identifierCollection, this); + startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); + startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); + getInterModuleEventBus().post(startChannelRequest); + } } /** - * Processes Phase 1 channel grants to allocate traffic channels and track overall channel usage. Generates - * decode events for each new channel that is allocated. + * Processes Phase 1 control-only channel grants to allocate traffic channels and track overall channel usage. + * Generates a tracked decode event for each new channel that is allocated. * - * Note: protected thread access to this method is controlled by the calling method. + * Note: this method is not thread safe and the calling method must protect access using mLock. * * @param apco25Channel for the traffic channel * @param serviceOptions for the traffic channel - optional can be null - * @param identifierCollection associated with the channel grant + * @param ic associated with the channel grant * @param decodeEventType to use * @param isDataChannelGrant indicator if this is a data channel grant * @param timestamp for the grant event. */ - private void processPhase1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, - IdentifierCollection identifierCollection, DecodeEventType decodeEventType, - boolean isDataChannelGrant, long timestamp) + private void processPhase1ControlChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, + IdentifierCollection ic, DecodeEventType decodeEventType, + boolean isDataChannelGrant, long timestamp, String context) { long frequency = apco25Channel.getDownlinkFrequency(); - P25ChannelGrantEvent event = mTS1ChannelGrantEventMap.get(frequency); + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, P25P1Message.TIMESLOT_1, timestamp); - // If the event is stale, remove it and allow a new event to be created. - if(isStaleEvent(event, timestamp)) + if(tracker != null && tracker.isSameCallCheckingToOnly(ic, timestamp)) { - event = null; - mTS1ChannelGrantEventMap.remove(frequency); - } + Identifier from = ic.getFromIdentifier(); - if(event != null && isSameCallUpdate(identifierCollection, event.getIdentifierCollection())) - { - Identifier from = identifierCollection.getFromIdentifier(); - - if(from != null) + if(from != null && tracker.isDifferentTalker(from)) { - Identifier currentFrom = event.getIdentifierCollection().getFromIdentifier(); - if(currentFrom != null && !from.equals(currentFrom)) - { - event.end(timestamp); - broadcast(event); - - event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) - .channelDescriptor(apco25Channel) - .details("CONTINUE - PHASE 1 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) - .build(); + P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + .channelDescriptor(apco25Channel) + .details("CONTINUE - PHASE 1 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) + .identifiers(ic) + .build(); - mTS1ChannelGrantEventMap.put(frequency, event); - } + tracker = new P25TrafficChannelEventTracker(event); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); } + //The tracked event can have an empty FROM identifier at start of call ... update here + tracker.addIdentifierIfMissing(from); + //update the ending timestamp so that the duration value is correctly calculated - event.update(timestamp); - broadcast(event); + tracker.updateDurationControl(timestamp); - //Even though we have an event, the initial channel grant may have been rejected. Check to see if there + broadcast(tracker); + + //Even though we have a tracked event, the initial channel grant may have been rejected. Check to see if there //is a traffic channel allocated. If not, allocate one and update the event description. if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && isDataChannelGrant)) { @@ -943,58 +984,49 @@ private void processPhase1ChannelGrant(APCO25Channel apco25Channel, ServiceOptio { if(isDataChannelGrant) { - event.setDetails("PHASE 1 DATA CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); + tracker.setDetails("PHASE 1 DATA CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); } else { - event.setDetails("PHASE 1 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); - } - event.setChannelDescriptor(apco25Channel); - broadcast(event); - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) - { - sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); + tracker.setDetails("PHASE 1 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); } - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - - ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel, - apco25Channel, identifierCollection, this); - startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); - startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); - getInterModuleEventBus().post(startChannelRequest); + tracker.addChannelDescriptorIfMissing(apco25Channel); + broadcast(tracker); + + requestTrafficChannelStart(trafficChannel, apco25Channel, ic, timestamp); + } + else + { + tracker.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED); } } return; } - if(mIgnoreDataCalls && isDataChannelGrant && event == null) + if(mIgnoreDataCalls && isDataChannelGrant && tracker == null) { - event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) .channelDescriptor(apco25Channel) .details("IGNORED: PHASE 1 DATA CALL " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) + .identifiers(ic) .build(); - mTS1ChannelGrantEventMap.put(frequency, event); - broadcast(event); + tracker = new P25TrafficChannelEventTracker(event); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); + broadcast(tracker); return; } - event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + String details = isDataChannelGrant ? "PHASE 1 DATA CHANNEL GRANT " : "PHASE 1 CHANNEL GRANT " + + (serviceOptions != null ? serviceOptions : ""); + + P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) .channelDescriptor(apco25Channel) - .details("PHASE 1 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) + .details(details) + .identifiers(ic) .build(); - - if(isDataChannelGrant) - { - event.setDetails("PHASE 1 DATA CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); - } - - mTS1ChannelGrantEventMap.put(frequency, event); + tracker = new P25TrafficChannelEventTracker(event); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); //Allocate a traffic channel for the downlink frequency if one isn't already allocated if(!mAllocatedTrafficChannelMap.containsKey(frequency)) @@ -1003,126 +1035,67 @@ private void processPhase1ChannelGrant(APCO25Channel apco25Channel, ServiceOptio if(trafficChannel == null) { - event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " - " + (event.getDetails() != null ? event.getDetails() : "")); + tracker.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " - " + (event.getDetails() != null ? event.getDetails() : "")); return; } - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) - { - sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); - } - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - - ChannelStartProcessingRequest startChannelRequest = - new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection, this); - startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); - startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); - - if(getInterModuleEventBus() != null) - { - getInterModuleEventBus().post(startChannelRequest); - } + requestTrafficChannelStart(trafficChannel, apco25Channel, ic, timestamp); } - broadcast(event); + broadcast(tracker); } /** - * Processes Phase 2 channel grants to allocate traffic channels and track overall channel usage. Generates - * decode events for each new channel that is allocated. + * Processes Phase 2 channel grants from both the control channel and from traffic channels to allocate traffic + * channels and track overall channel usage. Generates decode events for each new channel that is allocated. * - * Note: protected thread access to this method is controlled by the calling method. + * Note: this method is not thread safe and the calling method must protect access using mLock. * * @param apco25Channel for the traffic channel * @param serviceOptions for the traffic channel - optional can be null - * @param identifierCollection associated with the channel grant + * @param ic associated with the channel grant * @param decodeEventType to use for the event. * @param isDataChannelGrant indicator if this is a data channel grant * @param timestamp for the event */ - private DecodeEvent processPhase2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, - IdentifierCollection identifierCollection, DecodeEventType decodeEventType, + private void processPhase2ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, + IdentifierCollection ic, DecodeEventType decodeEventType, boolean isDataChannelGrant, long timestamp) { - if(mPhase2ScrambleParameters != null && identifierCollection instanceof MutableIdentifierCollection) + if(mPhase2ScrambleParameters != null && ic instanceof MutableIdentifierCollection) { - ((MutableIdentifierCollection)identifierCollection).silentUpdate(ScrambleParameterIdentifier.create(mPhase2ScrambleParameters)); + ((MutableIdentifierCollection)ic).silentUpdate(ScrambleParameterIdentifier.create(mPhase2ScrambleParameters)); } int timeslot = apco25Channel.getTimeslot(); long frequency = apco25Channel.getDownlinkFrequency(); + ic.setTimeslot(timeslot); - P25ChannelGrantEvent event = null; + P25TrafficChannelEventTracker tracker = getTrackerRemoveIfStale(apco25Channel, timestamp); - if(timeslot == P25P1Message.TIMESLOT_0 || timeslot == P25P1Message.TIMESLOT_1) + if(tracker != null && tracker.isSameCallCheckingToOnly(ic, timestamp)) { - event = mTS1ChannelGrantEventMap.get(frequency); - // If the event is stale, remove it and allow a new event to be created. - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS1ChannelGrantEventMap.remove(frequency); - } - } - else if(timeslot == P25P1Message.TIMESLOT_2) - { - event = mTS2ChannelGrantEventMap.get(frequency); - // If the event is stale, remove it and allow a new event to be created. - if(isStaleEvent(event, timestamp)) - { - event = null; - mTS2ChannelGrantEventMap.remove(frequency); - } - } - else - { - mLog.error("Ignoring: Invalid timeslot [" + timeslot + "] detected for P25 Phase 2 Channel Grant."); - return event; - } - - identifierCollection.setTimeslot(timeslot); - - if(event != null && isSameCallUpdate(identifierCollection, event.getIdentifierCollection())) - { - Identifier from = identifierCollection.getFromIdentifier(); + Identifier from = ic.getFromIdentifier(); - if(from != null) + if(from != null && tracker.isDifferentTalker(from)) { - Identifier currentFrom = event.getIdentifierCollection().getFromIdentifier(); - if(currentFrom != null && !from.equals(currentFrom)) - { - event.end(timestamp); - - P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) - .channelDescriptor(apco25Channel) - .details("CONTINUE - PHASE 2 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) - .build(); - - if(timeslot == P25P1Message.TIMESLOT_0 || timeslot == P25P1Message.TIMESLOT_1) - { - mTS1ChannelGrantEventMap.put(frequency, continuationGrantEvent); - } - else - { - mTS2ChannelGrantEventMap.put(frequency, continuationGrantEvent); - } + P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + .channelDescriptor(apco25Channel) + .details("CONTINUE - PHASE 2 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) + .identifiers(ic) + .timeslot(apco25Channel.getTimeslot()) + .build(); - broadcast(continuationGrantEvent); - } + tracker = new P25TrafficChannelEventTracker(continuationGrantEvent); + addTracker(tracker, frequency, timeslot); + broadcast(tracker); } //update the ending timestamp so that the duration value is correctly calculated - event.update(timestamp); - - //Update the event type in case this they change from unencrypted to encrypted. - event.setDecodeEventType(decodeEventType); - broadcast(event); + tracker.updateDurationControl(timestamp); + broadcast(tracker); - //Even though we have an event, the initial channel grant may have been rejected. Check to see if there + //Even though we have a tracked event, the initial channel grant may have been rejected. Check to see if there //is a traffic channel allocated. If not, allocate one and update the event description. if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && isDataChannelGrant) && (getCurrentControlFrequency() != frequency)) @@ -1131,64 +1104,44 @@ else if(timeslot == P25P1Message.TIMESLOT_2) if(trafficChannel != null) { - event.setDetails("PHASE 2 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); - event.setChannelDescriptor(apco25Channel); - broadcast(event); - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) - { - sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); - } - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - - //If we have valid scramble/randomizer parameters, set them in the decode config - if(mPhase2ScrambleParameters != null) - { - DecodeConfigP25Phase2 decodeConfig = (DecodeConfigP25Phase2)trafficChannel.getDecodeConfiguration(); - decodeConfig.setScrambleParameters(mPhase2ScrambleParameters.copy()); - } - - ChannelStartProcessingRequest startChannelRequest = - new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection, this); - startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); - startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); - getInterModuleEventBus().post(startChannelRequest); + tracker.setDetails("PHASE 2 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")); + tracker.addChannelDescriptorIfMissing(apco25Channel); + broadcast(tracker); + requestTrafficChannelStart(trafficChannel, apco25Channel, ic, timestamp); + } + else + { + tracker.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED); } } - return event; + return; } - if(mIgnoreDataCalls && isDataChannelGrant && event == null) + if(mIgnoreDataCalls && isDataChannelGrant && tracker == null) { - event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) .channelDescriptor(apco25Channel) .details("PHASE 2 DATA CALL IGNORED: " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) + .identifiers(ic) + .timeslot(apco25Channel.getTimeslot()) .build(); - //Phase 1 events get stored in TS1 only - mTS1ChannelGrantEventMap.put(frequency, event); - broadcast(event); - return event; + tracker = new P25TrafficChannelEventTracker(event); + addTracker(tracker, frequency, P25P1Message.TIMESLOT_1); + broadcast(tracker); + return; } - event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) + P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions) .channelDescriptor(apco25Channel) .details("PHASE 2 CHANNEL GRANT " + (serviceOptions != null ? serviceOptions : "")) - .identifiers(identifierCollection) + .identifiers(ic) + .timeslot(apco25Channel.getTimeslot()) .build(); - if(timeslot == P25P1Message.TIMESLOT_0 || timeslot == P25P1Message.TIMESLOT_1) - { - mTS1ChannelGrantEventMap.put(frequency, event); - } - else - { - mTS2ChannelGrantEventMap.put(frequency, event); - } + tracker = new P25TrafficChannelEventTracker(event); + addTracker(tracker, frequency, timeslot); //Allocate a traffic channel for the downlink frequency if one isn't already allocated if(!mAllocatedTrafficChannelMap.containsKey(frequency) && frequency != getCurrentControlFrequency()) @@ -1197,34 +1150,15 @@ else if(timeslot == P25P1Message.TIMESLOT_2) if(trafficChannel == null) { - event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " - IGNORED"); - return event; + tracker.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " - IGNORED"); } - - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner) - { - sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner()); - } - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelMap.put(frequency, trafficChannel); - - ChannelStartProcessingRequest startChannelRequest = - new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection, this); - startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); - startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); - - if(getInterModuleEventBus() == null) + else { - return event; + requestTrafficChannelStart(trafficChannel, apco25Channel, ic, timestamp); } - - getInterModuleEventBus().post(startChannelRequest); } - broadcast(event); - return event; + broadcast(tracker); } /** @@ -1236,7 +1170,7 @@ else if(timeslot == P25P1Message.TIMESLOT_2) */ private DecodeEventType getEventType(MacOpcode macOpcode, ServiceOptions serviceOptions, DecodeEventType current) { - boolean encrypted = serviceOptions != null ? serviceOptions.isEncrypted() : false; + boolean encrypted = serviceOptions != null && serviceOptions.isEncrypted(); DecodeEventType type = null; @@ -1312,48 +1246,33 @@ private DecodeEventType getEventType(MacOpcode macOpcode, ServiceOptions service return type; } - - /** * Creates a call event type description for the specified opcode and service options */ private DecodeEventType getEventType(Opcode opcode, ServiceOptions serviceOptions, DecodeEventType current) { - boolean encrypted = serviceOptions != null ? serviceOptions.isEncrypted() : false; + boolean encrypted = serviceOptions != null && serviceOptions.isEncrypted(); DecodeEventType type = null; if(opcode != null) { - switch(opcode) + type = switch(opcode) { - case OSP_GROUP_VOICE_CHANNEL_GRANT: - case OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE: - case OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: - type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; - break; - - case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: - case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: - type = encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; - break; - - case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: - case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE: - type = encrypted ? DecodeEventType.CALL_INTERCONNECT_ENCRYPTED : DecodeEventType.CALL_INTERCONNECT; - break; - - case OSP_SNDCP_DATA_CHANNEL_GRANT: - case OSP_GROUP_DATA_CHANNEL_GRANT: - case OSP_INDIVIDUAL_DATA_CHANNEL_GRANT: - type = encrypted ? DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL; - break; - - case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT: - case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_UPDATE: - type = encrypted ? DecodeEventType.CALL_PATCH_GROUP_ENCRYPTED : DecodeEventType.CALL_PATCH_GROUP; - break; - } + case OSP_GROUP_VOICE_CHANNEL_GRANT, OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE, + OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT -> + encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; + case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT, OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE -> + encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; + case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT, + OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE -> + encrypted ? DecodeEventType.CALL_INTERCONNECT_ENCRYPTED : DecodeEventType.CALL_INTERCONNECT; + case OSP_SNDCP_DATA_CHANNEL_GRANT, OSP_GROUP_DATA_CHANNEL_GRANT, OSP_INDIVIDUAL_DATA_CHANNEL_GRANT -> + encrypted ? DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL; + case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT, MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_UPDATE -> + encrypted ? DecodeEventType.CALL_PATCH_GROUP_ENCRYPTED : DecodeEventType.CALL_PATCH_GROUP; + default -> type; + }; } if(type == null) @@ -1416,59 +1335,6 @@ public void removeChannelEventListener() mChannelEventListener = null; } - /** - * Compares the TO role identifier(s) from each collection for equality. This is normally used to compare a call - * update where we only compare the TO role. - * - * @param existingIC containing a TO identifier - * @param nextIC containing a TO identifier - * @return true if both collections contain a TO identifier and the TO identifiers are the same value - */ - private boolean isSameCallUpdate(IdentifierCollection existingIC, IdentifierCollection nextIC) - { - Identifier existingTO = existingIC.getToIdentifier(); - Identifier nextTO = nextIC.getToIdentifier(); - return existingTO != null && existingTO.equals(nextTO); - } - - /** - * Compares the TO role identifier(s) from each collection for equality. This is normally used to compare a call - * update where we only compare the TO role. - * - * @param existingIC for the existing/previous event containing a TO identifier - * @param nextIC for the next event containing a TO identifier - * @return true if both collections contain a TO identifier and the TO identifiers are the same value - */ - private boolean isSameCallFull(IdentifierCollection existingIC, IdentifierCollection nextIC) - { - Identifier existingTO = existingIC.getToIdentifier(); - Identifier nextTO = nextIC.getToIdentifier(); - - if(existingTO != null && existingTO.equals(nextTO)) - { - Identifier existingFROM = existingIC.getFromIdentifier(); - - //If the FROM identifier hasn't yet been established, then this is the same call. We also ignore the - //talker alias as a call identifier since on L3Harris systems they can transmit the talker alias before - //they transmit the radio ID. - if(existingFROM == null || existingFROM.getForm() == Form.TALKER_ALIAS) - { - return true; - } - - Identifier nextFROM = nextIC.getFromIdentifier(); - - if(nextFROM != null && nextFROM.getForm() == Form.TALKER_ALIAS) - { - return true; - } - - return existingFROM.equals(nextFROM); - } - - return false; - } - /** * Implements the IDecodeEventProvider interface to provide channel events to an external listener. */ @@ -1530,13 +1396,13 @@ public Listener getMessageListener() mMessageListener = message -> { if(mPhase2ScrambleParameters == null && message.isValid()) { - if(message instanceof NetworkStatusBroadcast) + if(message instanceof NetworkStatusBroadcast nsb) { - mPhase2ScrambleParameters = ((NetworkStatusBroadcast)message).getScrambleParameters(); + mPhase2ScrambleParameters = nsb.getScrambleParameters(); } - else if(message instanceof AMBTCNetworkStatusBroadcast) + else if(message instanceof AMBTCNetworkStatusBroadcast nsb) { - mPhase2ScrambleParameters = ((AMBTCNetworkStatusBroadcast)message).getScrambleParameters(); + mPhase2ScrambleParameters = nsb.getScrambleParameters(); } } }; @@ -1603,7 +1469,7 @@ public void receive(ChannelEvent channelEvent) .findFirst() .ifPresent(frequency -> { mAllocatedTrafficChannelMap.remove(frequency); - mTS1ChannelGrantEventMap.remove(frequency); + removeTracker(frequency, P25P1Message.TIMESLOT_1); mAvailablePhase1TrafficChannelQueue.add(channel); }); break; @@ -1618,14 +1484,14 @@ public void receive(ChannelEvent channelEvent) //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 - P25ChannelGrantEvent event = mTS1ChannelGrantEventMap.get(rejectedFrequency); + P25TrafficChannelEventTracker tracker = getTracker(rejectedFrequency, P25P1Message.TIMESLOT_1); - if(event != null && !event.getDetails().contains(CHANNEL_START_REJECTED)) + if(tracker != null && !tracker.getEvent().getDetails().contains(CHANNEL_START_REJECTED)) { - event.setDetails(CHANNEL_START_REJECTED + " " + channelEvent.getDescription() + - (event.getDetails() != null ? " - " + event.getDetails() : "")); + tracker.setDetails(CHANNEL_START_REJECTED + " " + channelEvent.getDescription() + + (tracker.getEvent().getDetails() != null ? " - " + tracker.getEvent().getDetails() : "")); } - broadcast(event); + broadcast(tracker); }); break; } @@ -1652,7 +1518,9 @@ else if(mManagedPhase2TrafficChannels.contains(channel)) .ifPresent(frequency -> { mAllocatedTrafficChannelMap.remove(frequency); mAvailablePhase2TrafficChannelQueue.add(channel); - mTS1ChannelGrantEventMap.remove(frequency); + //Remove tracker from both timeslots + removeTracker(frequency, P25P1Message.TIMESLOT_1); + removeTracker(frequency, P25P1Message.TIMESLOT_2); }); break; case NOTIFICATION_PROCESSING_START_REJECTED: @@ -1664,20 +1532,20 @@ else if(mManagedPhase2TrafficChannels.contains(channel)) mAllocatedTrafficChannelMap.remove(rejectedFrequency); mAvailablePhase1TrafficChannelQueue.add(channel); - //Leave the event in the map so that it doesn't get recreated. The channel + //Leave the tracked event in the map so that it doesn't get recreated. The channel //processing manager set the 'tuner not available' in the details already - P25ChannelGrantEvent event1 = mTS1ChannelGrantEventMap.get(rejectedFrequency); - if (event1 != null) + P25TrafficChannelEventTracker tracker = getTracker(rejectedFrequency, P25P1Message.TIMESLOT_1); + if (tracker != null) { - broadcast(event1); + broadcast(tracker); } - //Leave the event in the map so that it doesn't get recreated. The channel + //Leave the tracked event in the map so that it doesn't get recreated. The channel //processing manager set the 'tuner not available' in the details already - P25ChannelGrantEvent event2 = mTS2ChannelGrantEventMap.get(rejectedFrequency); - if (event2 != null) + P25TrafficChannelEventTracker tracker2 = getTracker(rejectedFrequency, P25P1Message.TIMESLOT_2); + if (tracker2 != null) { - broadcast(event2); + broadcast(tracker2); } }); break; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/APCO25Channel.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/APCO25Channel.java index da34f1480..17fe2b741 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/APCO25Channel.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/APCO25Channel.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.identifier.channel; @@ -112,6 +109,36 @@ public static APCO25Channel create(int frequencyBand, int channelNumber) return new APCO25Channel(new P25Channel(frequencyBand, channelNumber)); } + /** + * Creates a new APCO-25 identifier with the same frequencyBand, and a different channelNumber representing the + * requested timeslot. + * @param requestedTimeslot to decorate as. + * @return decorated channel. + */ + public APCO25Channel decorateAs(int requestedTimeslot) + { + if(getTimeslot() == requestedTimeslot) + { + return this; + } + + P25Channel existing = getValue(); + + int channelNumber = existing.getChannelNumber(); + if(requestedTimeslot == 1) + { + channelNumber--; + } + else + { + channelNumber++; + } + + P25Channel decoratedChannel = new P25Channel(existing.getBandIdentifier(), channelNumber); + decoratedChannel.setFrequencyBand(existing.getFrequencyBand()); + return new APCO25Channel(decoratedChannel); + } + @Override public boolean equals(Object o) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/P25Channel.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/P25Channel.java index 8661f14d9..90c582b19 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/P25Channel.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/P25Channel.java @@ -42,6 +42,22 @@ public Protocol getProtocol() return Protocol.APCO25; } + /** + * Band identifier for this channel. + */ + public int getBandIdentifier() + { + return mBandIdentifier; + } + + /** + * Channel number for this channel. + */ + public int getChannelNumber() + { + return mChannelNumber; + } + /** * Frequency band information that backs this channel */ 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 8d617fd12..20fb940d1 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 @@ -66,15 +66,12 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.MotorolaTalkerAliasComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommandExtended; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUpdate; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUpdateExplicit; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdateExtended; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCNetworkStatusBroadcast; @@ -346,43 +343,42 @@ private void processTalkerAlias(LCHarrisTalkerAliasComplete talkerAlias) mTrafficChannelManager.getTalkerAliasManager().update(radioIdentifier, talkerAlias.getTalkerAlias()); } - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), talkerAlias.getTalkerAlias(), talkerAlias.getTimestamp()); + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), talkerAlias.getTalkerAlias(), talkerAlias.getTimestamp(), talkerAlias.toString()); } /** - * Commands the traffic channel manager to process a traffic channel grant and allocate a decoder to process the - * traffic channel. - * @param apco25Channel to allocate + * Processes a control channel directed traffic channel grant. + * @param channel to allocate * @param serviceOptions for the channel * @param identifiers to add to the current identifier collection * @param opcode that identifies the type of channel grant * @param timestamp when the channel grant occurred. */ - private void processChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions, - List identifiers, Opcode opcode, long timestamp) + private void processControlTrafficGrant(APCO25Channel channel, ServiceOptions serviceOptions, + List identifiers, Opcode opcode, long timestamp, String context) { - if(apco25Channel.getValue().getDownlinkFrequency() > 0) + if(channel.getValue().getDownlinkFrequency() > 0) { MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers, timestamp); mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); - mTrafficChannelManager.processP1ChannelGrant(apco25Channel, serviceOptions, mic, opcode, timestamp); + mTrafficChannelManager.processP1ControlDirectedChannelGrant(channel, serviceOptions, mic, opcode, timestamp, context); } } /** - * Process an update for another channel and send it to the traffic channel manager. + * Processes a control channel announced update to a traffic channel * @param channel where the call activity is happening. * @param serviceOptions for the call, optional null. * @param identifiers involved in the call * @param opcode for the update * @param timestamp of the message */ - private void processChannelUpdate(APCO25Channel channel, ServiceOptions serviceOptions, List identifiers, - Opcode opcode, long timestamp) + private void processControlAnnouncedTrafficUpdate(APCO25Channel channel, ServiceOptions serviceOptions, + List identifiers, Opcode opcode, long timestamp, String context) { MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers, timestamp); mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); - mTrafficChannelManager.processP1ChannelUpdate(channel, serviceOptions, mic, opcode, timestamp); + mTrafficChannelManager.processP1ControlAnnouncedTrafficUpdate(channel, serviceOptions, mic, opcode, timestamp, context); } /** @@ -434,8 +430,9 @@ else if(lcw.isEncrypted()) } mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType, - serviceOptions, getIdentifierCollection(), timestamp, null ); + MutableIdentifierCollection mic = getMutableIdentifierCollection(getIdentifierCollection().getIdentifiers(), timestamp); + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType, + serviceOptions, mic, timestamp, null, lcw.toString()); if(serviceOptions.isEncrypted()) { @@ -760,15 +757,15 @@ private void processAMBTCChannelGrantUpdate(AMBTCMessage ambtc) case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: if(ambtc instanceof AMBTCUnitToUnitVoiceServiceChannelGrantUpdate upd) { - processChannelUpdate(upd.getChannel(), upd.getServiceOptions(), upd.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlAnnouncedTrafficUpdate(upd.getChannel(), upd.getServiceOptions(), upd.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE: if(ambtc instanceof AMBTCTelephoneInterconnectChannelGrantUpdate upd) { - processChannelUpdate(upd.getChannel(), upd.getServiceOptions(), upd.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlAnnouncedTrafficUpdate(upd.getChannel(), upd.getServiceOptions(), upd.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; } @@ -784,64 +781,59 @@ private void processAMBTCChannelGrant(AMBTCMessage ambtc) case OSP_GROUP_DATA_CHANNEL_GRANT: if(ambtc instanceof AMBTCGroupDataChannelGrant gdcg) { - processChannelGrant(gdcg.getChannel(), gdcg.getServiceOptions(), gdcg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(gdcg.getChannel(), gdcg.getServiceOptions(), gdcg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case OSP_GROUP_VOICE_CHANNEL_GRANT: if(ambtc instanceof AMBTCGroupVoiceChannelGrant gvcg) { - processChannelGrant(gvcg.getChannel(), gvcg.getServiceOptions(), gvcg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(gvcg.getChannel(), gvcg.getServiceOptions(), gvcg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case OSP_INDIVIDUAL_DATA_CHANNEL_GRANT: if(ambtc instanceof AMBTCIndividualDataChannelGrant idcg) { - processChannelGrant(idcg.getChannel(), idcg.getServiceOptions(), idcg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(idcg.getChannel(), idcg.getServiceOptions(), idcg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: if(ambtc instanceof AMBTCTelephoneInterconnectChannelGrant ticg) { - processChannelGrant(ticg.getChannel(), ticg.getServiceOptions(), ticg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(ticg.getChannel(), ticg.getServiceOptions(), ticg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: if(ambtc instanceof AMBTCUnitToUnitVoiceServiceChannelGrant uuvscg) { - processChannelGrant(uuvscg.getChannel(), uuvscg.getServiceOptions(), uuvscg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(uuvscg.getChannel(), uuvscg.getServiceOptions(), uuvscg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT: if(ambtc instanceof AMBTCMotorolaGroupRegroupChannelGrant mgrcg) { - processChannelGrant(mgrcg.getChannel(), mgrcg.getServiceOptions(), mgrcg.getIdentifiers(), - ambtc.getHeader().getOpcode(), ambtc.getTimestamp()); + processControlTrafficGrant(mgrcg.getChannel(), mgrcg.getServiceOptions(), mgrcg.getIdentifiers(), + ambtc.getHeader().getOpcode(), ambtc.getTimestamp(), ambtc.toString()); } break; } } /** - * Processes a Header Data Unit message and starts a new call event. + * Processes a Header Data Unit message */ private void processHDU(IMessage message) { if(message.isValid() && message instanceof HDUMessage hdu) { HeaderData headerData = hdu.getHeaderData(); - ServiceOptions serviceOptions = headerData.isEncryptedAudio() ? - VoiceServiceOptions.createEncrypted() : VoiceServiceOptions.createUnencrypted(); - MutableIdentifierCollection mic = getMutableIdentifierCollection(hdu.getIdentifiers(), message.getTimestamp()); - String details = headerData.isEncryptedAudio() ? headerData.getEncryptionKey().toString() : null; - DecodeEventType type = headerData.isEncryptedAudio() ? DecodeEventType.CALL_ENCRYPTED : DecodeEventType.CALL; - mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), getCurrentChannel(), type, - serviceOptions, mic, message.getTimestamp(), details); + //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()) { @@ -881,24 +873,19 @@ else if(message instanceof LDU2Message ldu2) if(esp.isEncryptedAudio()) { getIdentifierCollection().update(esp.getIdentifiers()); - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), esp.getEncryptionKey(), - message.getTimestamp()); + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), esp.getEncryptionKey(), + message.getTimestamp(), ldu2.toString()); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ENCRYPTED)); } else { getIdentifierCollection().remove(Form.ENCRYPTION_KEY); - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), null, - message.getTimestamp()); + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), null, + message.getTimestamp(), ldu2.toString()); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); } } - else - { - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), null, message.getTimestamp()); - } } - } /** @@ -906,8 +893,12 @@ else if(message instanceof LDU2Message ldu2) */ private void processTDU(P25P1Message message) { - mTrafficChannelManager.closeP1CallEvent(getCurrentFrequency(), message.getTimestamp()); - getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + //If the TCM indicates that it closed the traffic call event, then remove the FROM users + if(mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message)) + { + getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + } + broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); } @@ -919,8 +910,12 @@ private void processTDU(P25P1Message message) */ private void processTDULC(P25P1Message message) { - mTrafficChannelManager.closeP1CallEvent(getCurrentFrequency(), message.getTimestamp()); - getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + boolean completed = mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message); + + if(completed) + { + getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + } if(message instanceof TDULinkControlMessage tdulc) { @@ -931,6 +926,7 @@ private void processTDULC(P25P1Message message) //Set the state to ACTIVE while the call continues in hangtime. The processLC() method will signal // the channel teardown when that happens. broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); + //Don't signal call end to the TCM or remove FROM identifiers unless the call has started on this traffic channel. processLC(lcw, message.getTimestamp(), true); } } @@ -1724,48 +1720,48 @@ private void processTSBKChannelGrantUpdate(TSBKMessage tsbk) case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_UPDATE: if(tsbk instanceof MotorolaGroupRegroupChannelUpdate pgvcgu) { - processChannelUpdate(pgvcgu.getChannel1(), null, Collections.singletonList(pgvcgu.getPatchGroup1()), - tsbk.getOpcode(), pgvcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(pgvcgu.getChannel1(), null, Collections.singletonList(pgvcgu.getPatchGroup1()), + tsbk.getOpcode(), pgvcgu.getTimestamp(), tsbk.toString()); if(pgvcgu.hasPatchGroup2()) { - processChannelUpdate(pgvcgu.getChannel2(), null, Collections.singletonList(pgvcgu.getPatchGroup2()), - tsbk.getOpcode(), pgvcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(pgvcgu.getChannel2(), null, Collections.singletonList(pgvcgu.getPatchGroup2()), + tsbk.getOpcode(), pgvcgu.getTimestamp(), tsbk.toString()); } } break; case OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE: if(tsbk instanceof GroupVoiceChannelGrantUpdate gvcgu) { - processChannelUpdate(gvcgu.getChannelA(), null, Collections.singletonList(gvcgu.getGroupAddressA()), - tsbk.getOpcode(), gvcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(gvcgu.getChannelA(), null, Collections.singletonList(gvcgu.getGroupAddressA()), + tsbk.getOpcode(), gvcgu.getTimestamp(), tsbk.toString()); if(gvcgu.hasGroupB()) { - processChannelGrant(gvcgu.getChannelB(), null, Collections.singletonList(gvcgu.getGroupAddressB()), - tsbk.getOpcode(), gvcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(gvcgu.getChannelB(), null, Collections.singletonList(gvcgu.getGroupAddressB()), + tsbk.getOpcode(), gvcgu.getTimestamp(), tsbk.toString()); } } break; case OSP_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: if(tsbk instanceof GroupVoiceChannelGrantUpdateExplicit gvcgue) { - processChannelUpdate(gvcgue.getChannel(), gvcgue.getServiceOptions(), gvcgue.getIdentifiers(), - tsbk.getOpcode(), gvcgue.getTimestamp()); + processControlAnnouncedTrafficUpdate(gvcgue.getChannel(), gvcgue.getServiceOptions(), gvcgue.getIdentifiers(), + tsbk.getOpcode(), gvcgue.getTimestamp(), tsbk.toString()); } break; case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE: if(tsbk instanceof TelephoneInterconnectVoiceChannelGrantUpdate tivcgu) { - processChannelUpdate(tivcgu.getChannel(), tivcgu.getServiceOptions(), tivcgu.getIdentifiers(), - tsbk.getOpcode(), tivcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(tivcgu.getChannel(), tivcgu.getServiceOptions(), tivcgu.getIdentifiers(), + tsbk.getOpcode(), tivcgu.getTimestamp(), tsbk.toString()); } break; case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE: if(tsbk instanceof UnitToUnitVoiceChannelGrantUpdate uuvcgu) { - processChannelUpdate(uuvcgu.getChannel(), null, uuvcgu.getIdentifiers(), tsbk.getOpcode(), - uuvcgu.getTimestamp()); + processControlAnnouncedTrafficUpdate(uuvcgu.getChannel(), null, uuvcgu.getIdentifiers(), tsbk.getOpcode(), + uuvcgu.getTimestamp(), tsbk.toString()); } break; } @@ -1781,43 +1777,43 @@ private void processTSBKChannelGrant(TSBKMessage tsbk) case MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT: if(tsbk instanceof MotorolaGroupRegroupChannelGrant mgrcg) { - processChannelGrant(mgrcg.getChannel(), mgrcg.getServiceOptions(), mgrcg.getIdentifiers(), tsbk.getOpcode(), - mgrcg.getTimestamp()); + processControlTrafficGrant(mgrcg.getChannel(), mgrcg.getServiceOptions(), mgrcg.getIdentifiers(), tsbk.getOpcode(), + mgrcg.getTimestamp(), tsbk.toString()); } break; case OSP_GROUP_DATA_CHANNEL_GRANT: if(tsbk instanceof GroupDataChannelGrant gdcg) { - processChannelGrant(gdcg.getChannel(), gdcg.getDataServiceOptions(), gdcg.getIdentifiers(), - tsbk.getOpcode(), gdcg.getTimestamp()); + processControlTrafficGrant(gdcg.getChannel(), gdcg.getDataServiceOptions(), gdcg.getIdentifiers(), + tsbk.getOpcode(), gdcg.getTimestamp(), tsbk.toString()); } break; case OSP_GROUP_VOICE_CHANNEL_GRANT: if(tsbk instanceof GroupVoiceChannelGrant gvcg) { - processChannelGrant(gvcg.getChannel(), gvcg.getServiceOptions(), gvcg.getIdentifiers(), tsbk.getOpcode(), - gvcg.getTimestamp()); + processControlTrafficGrant(gvcg.getChannel(), gvcg.getServiceOptions(), gvcg.getIdentifiers(), tsbk.getOpcode(), + gvcg.getTimestamp(), tsbk.toString()); } break; case OSP_SNDCP_DATA_CHANNEL_GRANT: if(tsbk instanceof SNDCPDataChannelGrant dcg) { - processChannelGrant(dcg.getChannel(), dcg.getServiceOptions(), dcg.getIdentifiers(), tsbk.getOpcode(), - dcg.getTimestamp()); + processControlTrafficGrant(dcg.getChannel(), dcg.getServiceOptions(), dcg.getIdentifiers(), tsbk.getOpcode(), + dcg.getTimestamp(), tsbk.toString()); } break; case OSP_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT: if(tsbk instanceof UnitToUnitVoiceChannelGrant uuvcg) { - processChannelGrant(uuvcg.getChannel(), null, uuvcg.getIdentifiers(), tsbk.getOpcode(), - uuvcg.getTimestamp()); + processControlTrafficGrant(uuvcg.getChannel(), null, uuvcg.getIdentifiers(), tsbk.getOpcode(), + uuvcg.getTimestamp(), tsbk.toString()); } break; case OSP_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT: if(tsbk instanceof TelephoneInterconnectVoiceChannelGrant tivcg) { - processChannelGrant(tivcg.getChannel(), tivcg.getServiceOptions(), tivcg.getIdentifiers(), tsbk.getOpcode(), - tivcg.getTimestamp()); + processControlTrafficGrant(tivcg.getChannel(), tivcg.getServiceOptions(), tivcg.getIdentifiers(), tsbk.getOpcode(), + tivcg.getTimestamp(), tsbk.toString()); } break; } @@ -1843,7 +1839,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } else { @@ -1854,14 +1850,14 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(lcw instanceof LCMotorolaTalkComplete tc) { getIdentifierCollection().update(tc.getAddress()); - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), tc.getAddress(), timestamp); - closeCurrentCallEvent(timestamp); + //Note: don't update the traffic channel manager because the current event is already in closeout + closeCurrentCallEvent(timestamp, lcw.toString()); } break; //Call termination case CALL_TERMINATION_OR_CANCELLATION: - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); //Note: we only broadcast an END state if this is a network-commanded channel teardown if(lcw instanceof LCCallTermination lcct && lcct.isNetworkCommandedTeardown()) @@ -1870,38 +1866,17 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator } break; - //Calls in-progress on another channel + //Voice Channel Update messages - indicates calls in-progress on another channel - ignored case GROUP_VOICE_CHANNEL_UPDATE: if(isTerminator) { - closeCurrentCallEvent(timestamp); - } - - if(lcw instanceof LCGroupVoiceChannelUpdate vcu) - { - MutableIdentifierCollection mic = getMutableIdentifierCollection(vcu.getGroupAddressA(), timestamp); - mTrafficChannelManager.processP1ChannelUpdate(vcu.getChannelA(), null, mic, - null, timestamp); - - if(vcu.hasChannelB()) - { - MutableIdentifierCollection micB = getMutableIdentifierCollection(vcu.getGroupAddressB(), timestamp); - mTrafficChannelManager.processP1ChannelUpdate(vcu.getChannelB(), null, micB, - null, timestamp); - } + closeCurrentCallEvent(timestamp, lcw.toString()); } break; case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT: if(isTerminator) { - closeCurrentCallEvent(timestamp); - } - - if(lcw instanceof LCGroupVoiceChannelUpdateExplicit vcu) - { - MutableIdentifierCollection mic = getMutableIdentifierCollection(vcu.getGroupAddress(), timestamp); - mTrafficChannelManager.processP1ChannelUpdate(vcu.getChannel(), vcu.getServiceOptions(), mic, - null, timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } break; @@ -1923,7 +1898,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } mNetworkConfigurationMonitor.process(lcw); @@ -1945,7 +1920,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } mNetworkConfigurationMonitor.process(lcw); @@ -1968,7 +1943,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } mNetworkConfigurationMonitor.process(lcw); @@ -1990,7 +1965,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } mNetworkConfigurationMonitor.process(lcw); @@ -2006,7 +1981,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case SYSTEM_SERVICE_BROADCAST: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } mNetworkConfigurationMonitor.process(lcw); break; @@ -2019,23 +1994,17 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator mPatchGroupManager.removePatchGroups(lcw.getIdentifiers()); break; case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_UPDATE: + //Voice Channel Update message - indicates calls in-progress on another channel - ignored if(isTerminator) { - closeCurrentCallEvent(timestamp); - } - - if(lcw instanceof LCMotorolaGroupRegroupVoiceChannelUpdate vcu) - { - MutableIdentifierCollection mic = getMutableIdentifierCollection(vcu.getSupergroupAddress(), timestamp); - mTrafficChannelManager.processP1ChannelUpdate(vcu.getChannel(), vcu.getServiceOptions(), mic, - null, timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } break; case MOTOROLA_TALKER_ALIAS_HEADER: case MOTOROLA_TALKER_ALIAS_DATA_BLOCK: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } break; @@ -2043,14 +2012,14 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case CALL_ALERT: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Call Alert"); break; case EXTENDED_FUNCTION_COMMAND: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCExtendedFunctionCommand efc) { @@ -2062,7 +2031,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case EXTENDED_FUNCTION_COMMAND_EXTENDED: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCExtendedFunctionCommandExtended efce) { @@ -2072,14 +2041,14 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case GROUP_AFFILIATION_QUERY: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.QUERY, "Group Affiliation"); break; case MESSAGE_UPDATE: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCMessageUpdate mu) { @@ -2090,7 +2059,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case MESSAGE_UPDATE_EXTENDED: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCMessageUpdateExtended mue) { @@ -2101,14 +2070,14 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case STATUS_QUERY: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.QUERY, "Status"); break; case STATUS_UPDATE: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCStatusUpdate su) { @@ -2119,7 +2088,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case STATUS_UPDATE_EXTENDED: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCStatusUpdateExtended sue) { @@ -2130,7 +2099,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } if(lcw instanceof LCTelephoneInterconnectAnswerRequest tiar) { @@ -2141,21 +2110,21 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case UNIT_AUTHENTICATION_COMMAND: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.COMMAND, "Authenticate Unit"); break; case UNIT_REGISTRATION_COMMAND: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.COMMAND, "Unit Registration"); break; case UNIT_TO_UNIT_ANSWER_REQUEST: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } broadcastEvent(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Unit-to-Unit Answer Request"); break; @@ -2174,7 +2143,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case MOTOROLA_UNIT_GPS: if(lcw instanceof LCMotorolaUnitGPS gps) { - mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), gps.getLocation(), timestamp); + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), gps.getLocation(), timestamp, lcw.toString()); MutableIdentifierCollection mic = getMutableIdentifierCollection(gps.getIdentifiers(), timestamp); PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp) @@ -2190,7 +2159,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } break; case SOURCE_ID_EXTENSION: @@ -2199,7 +2168,7 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator default: if(isTerminator) { - closeCurrentCallEvent(timestamp); + closeCurrentCallEvent(timestamp, lcw.toString()); } // if(lcw.getVendor().isLoggable()) @@ -2215,11 +2184,11 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator /** * Closes the call event on the current channel. - * @param timestamp + * @param timestamp for the closure */ - private void closeCurrentCallEvent(long timestamp) + private void closeCurrentCallEvent(long timestamp, String context) { - mTrafficChannelManager.closeP1CallEvent(getCurrentFrequency(), timestamp); + mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), timestamp, context); getIdentifierCollection().remove(IdentifierClass.USER); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCGroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCGroupVoiceChannelUser.java index 796446edc..5035c5856 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCGroupVoiceChannelUser.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCGroupVoiceChannelUser.java @@ -151,7 +151,7 @@ public Identifier getSourceAddress() { if(mSourceAddress == null) { - if(isExtensionRequired() && mSourceIdExtension != null) + if(isExtensionRequired() && mSourceIdExtension != null && mSourceIdExtension.isValidExtendedSource()) { mSourceAddress = APCO25FullyQualifiedRadioIdentifier.createFrom(getInt(SOURCE_ADDRESS), mSourceIdExtension.getWACN(), mSourceIdExtension.getSystem(), mSourceIdExtension.getId()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCSourceIDExtension.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCSourceIDExtension.java index 76f9c86af..2526e12fd 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCSourceIDExtension.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/standard/LCSourceIDExtension.java @@ -65,6 +65,16 @@ public String toString() return sb.toString(); } + /** + * Indicates that at least one of the WACN, System, or ID values are non-zero. + * @return + */ + public boolean isValidExtendedSource() + { + return getWACN() != 0 && getSystem() != 0 && getId() != 0; + } + + @Override public boolean isTerminator() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java index b96287700..1b709f3a3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java @@ -38,7 +38,6 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.module.ProcessingChain; -import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager; import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; @@ -123,7 +122,7 @@ public void setSampleRate(double sampleRate) //The Costas Loop receives symbol-inversion correction requests when detected. //The PLL gain monitor receives sync detect/loss signals from the message framer - mMessageFramer = new P25P2MessageFramer(mCostasLoop, DecoderType.P25_PHASE2.getProtocol().getBitRate()); + mMessageFramer = new P25P2MessageFramer(mCostasLoop); if(mDecodeConfigP25Phase2 !=null) { @@ -146,8 +145,6 @@ public void setSampleRate(double sampleRate) @Override public void receive(ComplexSamples samples) { - mMessageFramer.setCurrentTime(System.currentTimeMillis()); - float[] i = mIBasebandFilter.filter(samples.i()); float[] q = mQBasebandFilter.filter(samples.q()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKInstrumented.java index ab269b928..6bdcb3eb1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSKInstrumented.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,8 +58,6 @@ public DQPSKGardnerDemodulatorInstrumented getDemodulator() @Override public void receive(ComplexSamples samples) { - mMessageFramer.setCurrentTime(System.currentTimeMillis()); - float[] i = mIBasebandFilter.filter(samples.i()); float[] q = mQBasebandFilter.filter(samples.q()); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java index 1baf0982e..a7609abf0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java @@ -55,7 +55,6 @@ import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence; import io.github.dsheirer.module.decode.p25.phase2.message.mac.IP25ChannelGrantDetailProvider; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacOpcode; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacPduType; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.AcknowledgeResponseFNEAbbreviated; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.AcknowledgeResponseFNEExtended; @@ -127,7 +126,6 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; -import io.github.dsheirer.module.decode.p25.reference.ServiceOptions; import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions; import io.github.dsheirer.protocol.Protocol; import java.util.Collections; @@ -191,7 +189,7 @@ public void reset() protected void resetState() { super.resetState(); - closeCurrentCallEvent(true, false); + mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), System.currentTimeMillis(), "RESET STATE INVOKED"); mEndPttOnFacchCounter = 0; } @@ -238,13 +236,16 @@ else if(message instanceof AbstractVoiceTimeslot) { broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); } + + //If we're tracking the call event, update the duration on it + mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp()); } else if(message instanceof EncryptionSynchronizationSequence ess) { //We don't send any state events for this message since it can only occur in conjunction with //an audio frame that already sends the call state event getIdentifierCollection().update(message.getIdentifiers()); - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), ess.getEncryptionKey(), ess.getTimestamp()); + mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), ess.getEncryptionKey(), ess.getTimestamp()); if(ess.isEncrypted()) { @@ -257,11 +258,6 @@ else if(message instanceof EncryptionSynchronizationSequence ess) } else if(message instanceof MotorolaTalkerAliasComplete tac) { - //Debug -// if(!mTrafficChannelManager.getTalkerAliasManager().hasAlias(tac.getRadio())) -// { -// System.out.println(tac); -// } mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias()); } } @@ -279,6 +275,11 @@ private void processMacMessage(MacMessage message) case MAC_0_SIGNAL: continueState(State.CONTROL); break; + case MAC_6_HANGTIME: + //During hangtime, the from talker is no longer involved in the call ... remove it because sometimes + //we don't get the end PTT to signal the end. + getIdentifierCollection().remove(Role.FROM); + break; } MacStructure mac = message.getMacStructure(); @@ -302,9 +303,19 @@ private void processMacMessage(MacMessage message) case TDMA_01_GROUP_VOICE_CHANNEL_USER_ABBREVIATED: case TDMA_02_UNIT_TO_UNIT_VOICE_CHANNEL_USER_ABBREVIATED: case TDMA_03_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER: + case TDMA_21_GROUP_VOICE_CHANNEL_USER_EXTENDED: + case TDMA_22_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED: processChannelUser(message, mac); break; case TDMA_05_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_IMPLICIT: + case TDMA_25_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT: + case PHASE1_42_GROUP_VOICE_CHANNEL_GRANT_UPDATE_IMPLICIT: + case PHASE1_46_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_ABBREVIATED: + case PHASE1_49_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE_IMPLICIT: + case PHASE1_C3_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: + case PHASE1_C6_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED_VCH: + case PHASE1_C7_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED_LCCH: + case PHASE1_C9_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: processChannelGrantUpdate(message, mac); break; case TDMA_08_NULL_AVOID_ZERO_BIAS: @@ -317,13 +328,6 @@ private void processMacMessage(MacMessage message) case TDMA_12_INDIVIDUAL_PAGING_WITH_PRIORITY: processPaging(message, mac); break; - case TDMA_21_GROUP_VOICE_CHANNEL_USER_EXTENDED: - case TDMA_22_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED: - processChannelUser(message, mac); - break; - case TDMA_25_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT: - processChannelGrantUpdate(message, mac); - break; case TDMA_30_POWER_CONTROL_SIGNAL_QUALITY: processPowerControl(message, mac); break; @@ -337,33 +341,29 @@ private void processMacMessage(MacMessage message) case PHASE1_PARTITION_1_UNKNOWN_OPCODE: break; case PHASE1_40_GROUP_VOICE_CHANNEL_GRANT_IMPLICIT: + case PHASE1_44_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_ABBREVIATED: + case PHASE1_48_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_IMPLICIT: + case PHASE1_54_SNDCP_DATA_CHANNEL_GRANT: + case PHASE1_C8_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_EXPLICIT: + case PHASE1_C0_GROUP_VOICE_CHANNEL_GRANT_EXPLICIT: + case PHASE1_C4_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_EXTENDED_VCH: + case PHASE1_CF_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_EXTENDED_LCCH: processChannelGrant(message, mac); break; case PHASE1_41_GROUP_VOICE_SERVICE_REQUEST: //Inbound only. break; - case PHASE1_42_GROUP_VOICE_CHANNEL_GRANT_UPDATE_IMPLICIT: - processChannelGrantUpdate(message, mac); - break; - case PHASE1_44_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_ABBREVIATED: - processChannelGrant(message, mac); - break; case PHASE1_45_UNIT_TO_UNIT_ANSWER_REQUEST_ABBREVIATED: - processAnswer(message, mac); - break; - case PHASE1_46_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_ABBREVIATED: - processChannelGrantUpdate(message, mac); - break; - case PHASE1_48_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_IMPLICIT: - processChannelGrant(message, mac); - break; - case PHASE1_49_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE_IMPLICIT: - processChannelGrantUpdate(message, mac); - break; case PHASE1_4A_TELEPHONE_INTERCONNECT_ANSWER_RESPONSE: + case PHASE1_C5_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED: processAnswer(message, mac); break; case PHASE1_4C_RADIO_UNIT_MONITOR_COMMAND_ABBREVIATED: + case PHASE1_5D_RADIO_UNIT_MONITOR_COMMAND_OBSOLETE: + case PHASE1_5E_RADIO_UNIT_MONITOR_ENHANCED_COMMAND_ABBREVIATED: + case PHASE1_CC_RADIO_UNIT_MONITOR_COMMAND_EXTENDED_VCH: + case PHASE1_CD_RADIO_UNIT_MONITOR_COMMAND_EXTENDED_LCCH: + case PHASE1_DE_RADIO_UNIT_MONITOR_ENHANCED_COMMAND_EXTENDED: processRadioUnitMonitor(message, mac); break; case PHASE1_52_SNDCP_DATA_CHANNEL_REQUEST: @@ -372,35 +372,37 @@ private void processMacMessage(MacMessage message) case PHASE1_53_SNDCP_DATA_PAGE_RESPONSE: //Ignore - this is an inbound request by the SU break; - case PHASE1_54_SNDCP_DATA_CHANNEL_GRANT: - processChannelGrant(message, mac); - break; case PHASE1_55_SNDCP_DATA_PAGE_REQUEST: processDataPageRequest(message, mac); break; case PHASE1_58_STATUS_UPDATE_ABBREVIATED: - processStatus(message, mac); - break; case PHASE1_5A_STATUS_QUERY_ABBREVIATED: + case PHASE1_D8_STATUS_UPDATE_EXTENDED_VCH: + case PHASE1_D9_STATUS_UPDATE_EXTENDED_LCCH: + case PHASE1_DA_STATUS_QUERY_EXTENDED_VCH: + case PHASE1_DB_STATUS_QUERY_EXTENDED_LCCH: processStatus(message, mac); break; case PHASE1_5C_MESSAGE_UPDATE_ABBREVIATED: + case PHASE1_CE_MESSAGE_UPDATE_EXTENDED_LCCH: + case PHASE1_DC_MESSAGE_UPDATE_EXTENDED_VCH: processMessageUpdate(message, mac); break; - case PHASE1_5D_RADIO_UNIT_MONITOR_COMMAND_OBSOLETE: - case PHASE1_5E_RADIO_UNIT_MONITOR_ENHANCED_COMMAND_ABBREVIATED: - processRadioUnitMonitor(message, mac); - break; case PHASE1_5F_CALL_ALERT_ABBREVIATED: + case PHASE1_CB_CALL_ALERT_EXTENDED_LCCH: + case PHASE1_DF_CALL_ALERT_EXTENDED_VCH: processCallAlert(message, mac); break; case PHASE1_60_ACKNOWLEDGE_RESPONSE_FNE_ABBREVIATED: + case PHASE1_E0_ACKNOWLEDGE_RESPONSE_FNE_EXTENDED: processAcknowledge(message, mac); break; case PHASE1_61_QUEUED_RESPONSE: processQueued(message, mac); break; case PHASE1_64_EXTENDED_FUNCTION_COMMAND_ABBREVIATED: + case PHASE1_E4_EXTENDED_FUNCTION_COMMAND_EXTENDED_VCH: + case PHASE1_E5_EXTENDED_FUNCTION_COMMAND_EXTENDED_LCCH: processExtendedFunctionCommand(message, mac); break; case PHASE1_67_DENY_RESPONSE: @@ -408,6 +410,8 @@ private void processMacMessage(MacMessage message) break; case PHASE1_68_GROUP_AFFILIATION_RESPONSE_ABBREVIATED: case PHASE1_6A_GROUP_AFFILIATION_QUERY_ABBREVIATED: + case PHASE1_E8_GROUP_AFFILIATION_RESPONSE_EXTENDED: + case PHASE1_EA_GROUP_AFFILIATION_QUERY_EXTENDED: processAffiliation(message, mac); break; case PHASE1_6B_LOCATION_REGISTRATION_RESPONSE: @@ -416,6 +420,7 @@ private void processMacMessage(MacMessage message) case PHASE1_6C_UNIT_REGISTRATION_RESPONSE_ABBREVIATED: case PHASE1_6D_UNIT_REGISTRATION_COMMAND_ABBREVIATED: case PHASE1_6F_DEREGISTRATION_ACKNOWLEDGE: + case PHASE1_EC_UNIT_REGISTRATION_RESPONSE_EXTENDED: processUnitRegistration(message, mac); break; case PHASE1_70_SYNCHRONIZATION_BROADCAST: @@ -423,6 +428,7 @@ private void processMacMessage(MacMessage message) break; case PHASE1_71_AUTHENTICATION_DEMAND: case PHASE1_72_AUTHENTICATION_FNE_RESPONSE_ABBREVIATED: + case PHASE1_F2_AUTHENTICATION_FNE_RESPONSE_EXTENDED: processAuthentication(message, mac); break; case PHASE1_76_ROAMING_ADDRESS_COMMAND: @@ -431,19 +437,24 @@ private void processMacMessage(MacMessage message) break; case PHASE1_73_IDENTIFIER_UPDATE_TDMA_ABBREVIATED: case PHASE1_74_IDENTIFIER_UPDATE_V_UHF: - processNetwork(message, mac); - break; - case PHASE1_75_TIME_AND_DATE_ANNOUNCEMENT: - //Ignore - break; case PHASE1_78_SYSTEM_SERVICE_BROADCAST: case PHASE1_79_SECONDARY_CONTROL_CHANNEL_BROADCAST_IMPLICIT: case PHASE1_7A_RFSS_STATUS_BROADCAST_IMPLICIT: case PHASE1_7B_NETWORK_STATUS_BROADCAST_IMPLICIT: case PHASE1_7C_ADJACENT_STATUS_BROADCAST_IMPLICIT: case PHASE1_7D_IDENTIFIER_UPDATE: + case PHASE1_D6_SNDCP_DATA_CHANNEL_ANNOUNCEMENT: + case PHASE1_E9_SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: + case PHASE1_F3_IDENTIFIER_UPDATE_TDMA_EXTENDED: + case PHASE1_FA_RFSS_STATUS_BROADCAST_EXPLICIT: + case PHASE1_FB_NETWORK_STATUS_BROADCAST_EXPLICIT: + case PHASE1_FC_ADJACENT_STATUS_BROADCAST_EXPLICIT: + case PHASE1_FE_ADJACENT_STATUS_BROADCAST_EXTENDED_EXPLICIT: processNetwork(message, mac); break; + case PHASE1_75_TIME_AND_DATE_ANNOUNCEMENT: + //Ignore + break; /** * Partition 3 Opcodes @@ -458,88 +469,6 @@ private void processMacMessage(MacMessage message) } processChannelUser(message, mac); break; - case PHASE1_C0_GROUP_VOICE_CHANNEL_GRANT_EXPLICIT: - processChannelGrant(message, mac); - break; - case PHASE1_C3_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: - processChannelGrantUpdate(message, mac); - break; - case PHASE1_C4_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_EXTENDED_VCH: - processChannelGrant(message, mac); - break; - case PHASE1_C5_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED: - processAnswer(message, mac); - break; - case PHASE1_C6_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED_VCH: - case PHASE1_C7_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED_LCCH: - processChannelGrantUpdate(message, mac); - break; - case PHASE1_C8_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_EXPLICIT: - processChannelGrant(message, mac); - break; - case PHASE1_C9_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: - processChannelGrantUpdate(message, mac); - break; - case PHASE1_CB_CALL_ALERT_EXTENDED_LCCH: - processCallAlert(message, mac); - break; - case PHASE1_CC_RADIO_UNIT_MONITOR_COMMAND_EXTENDED_VCH: - case PHASE1_CD_RADIO_UNIT_MONITOR_COMMAND_EXTENDED_LCCH: - processRadioUnitMonitor(message, mac); - break; - case PHASE1_CE_MESSAGE_UPDATE_EXTENDED_LCCH: - processMessageUpdate(message, mac); - break; - case PHASE1_CF_UNIT_TO_UNIT_VOICE_SERVICE_CHANNEL_GRANT_EXTENDED_LCCH: - processChannelGrant(message, mac); - break; - case PHASE1_D6_SNDCP_DATA_CHANNEL_ANNOUNCEMENT: - processNetwork(message, mac); - break; - case PHASE1_D8_STATUS_UPDATE_EXTENDED_VCH: - case PHASE1_D9_STATUS_UPDATE_EXTENDED_LCCH: - case PHASE1_DA_STATUS_QUERY_EXTENDED_VCH: - case PHASE1_DB_STATUS_QUERY_EXTENDED_LCCH: - processStatus(message, mac); - break; - case PHASE1_DC_MESSAGE_UPDATE_EXTENDED_VCH: - processMessageUpdate(message, mac); - break; - case PHASE1_DE_RADIO_UNIT_MONITOR_ENHANCED_COMMAND_EXTENDED: - processRadioUnitMonitor(message, mac); - break; - case PHASE1_DF_CALL_ALERT_EXTENDED_VCH: - processCallAlert(message, mac); - break; - case PHASE1_E0_ACKNOWLEDGE_RESPONSE_FNE_EXTENDED: - processAcknowledge(message, mac); - break; - case PHASE1_E4_EXTENDED_FUNCTION_COMMAND_EXTENDED_VCH: - case PHASE1_E5_EXTENDED_FUNCTION_COMMAND_EXTENDED_LCCH: - processExtendedFunctionCommand(message, mac); - break; - case PHASE1_E8_GROUP_AFFILIATION_RESPONSE_EXTENDED: - processAffiliation(message, mac); - break; - case PHASE1_E9_SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: - processNetwork(message, mac); - break; - case PHASE1_EA_GROUP_AFFILIATION_QUERY_EXTENDED: - processAffiliation(message, mac); - break; - case PHASE1_EC_UNIT_REGISTRATION_RESPONSE_EXTENDED: - processUnitRegistration(message, mac); - break; - case PHASE1_F2_AUTHENTICATION_FNE_RESPONSE_EXTENDED: - processAuthentication(message, mac); - break; - case PHASE1_F3_IDENTIFIER_UPDATE_TDMA_EXTENDED: - case PHASE1_FA_RFSS_STATUS_BROADCAST_EXPLICIT: - case PHASE1_FB_NETWORK_STATUS_BROADCAST_EXPLICIT: - case PHASE1_FC_ADJACENT_STATUS_BROADCAST_EXPLICIT: - case PHASE1_FE_ADJACENT_STATUS_BROADCAST_EXTENDED_EXPLICIT: - processNetwork(message, mac); - break; /** * Partition 2 Opcodes @@ -835,7 +764,7 @@ private void processChannelGrant(MacMessage message, MacStructure mac) //Add the traffic channel to the IC ic.update(cgdp.getChannel()); mTrafficChannelManager.processP2ChannelGrant(cgdp.getChannel(), cgdp.getServiceOptions(), ic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } } } @@ -864,7 +793,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getGroupAddress1(), message.getTimestamp()); mic.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), cgu.getServiceOptions1(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } if(cgu.hasGroup2()) @@ -876,7 +805,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic2 = getIdentifierCollectionForUser(cgu.getGroupAddress2(), message.getTimestamp()); mic2.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), cgu.getServiceOptions2(), mic2, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } @@ -889,7 +818,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic3 = getIdentifierCollectionForUser(cgu.getGroupAddress3(), message.getTimestamp()); mic3.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), cgu.getServiceOptions3(), mic3, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } } @@ -904,7 +833,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getGroupAddress1(), message.getTimestamp()); mic.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), cgu.getServiceOptions1(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } if(cgu.hasGroup2()) @@ -916,7 +845,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic2 = getIdentifierCollectionForUser(cgu.getGroupAddress2(), message.getTimestamp()); mic2.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), cgu.getServiceOptions2(), mic2, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } } @@ -934,7 +863,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getGroupAddress1(), message.getTimestamp()); mic.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), serviceOptions, mic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } if(cgu.hasGroup2()) @@ -946,7 +875,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic2 = getIdentifierCollectionForUser(cgu.getGroupAddress2(), message.getTimestamp()); mic2.update(cgu.getChannel1()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel1(), serviceOptions, mic2, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } } @@ -963,7 +892,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUsers(cgu.getIdentifiers(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), serviceOptions, mic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } } break; @@ -979,7 +908,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUsers(cgu.getIdentifiers(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), serviceOptions, mic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } } break; @@ -993,7 +922,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getGroupAddress(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), cgu.getServiceOptions(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } break; @@ -1009,7 +938,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUsers(cgu.getIdentifiers(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), serviceOptions, mic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } } break; @@ -1023,7 +952,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUsers(cgu.getIdentifiers(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), cgu.getServiceOptions(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } break; @@ -1037,7 +966,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUsers(cgu.getIdentifiers(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), cgu.getServiceOptions(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } break; @@ -1052,7 +981,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getPatchgroup(), message.getTimestamp()); mic.update(cgu.getChannel()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannel(), cgu.getServiceOptions(), mic, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } break; @@ -1070,7 +999,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic = getIdentifierCollectionForUser(cgu.getPatchgroupA(), message.getTimestamp()); mic.update(cgu.getChannelA()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannelA(), serviceOptions, mic, mac.getOpcode(), - message.getTimestamp()); + message.getTimestamp(), mac.toString()); } if(cgu.hasPatchgroupB()) @@ -1083,7 +1012,7 @@ private void processChannelGrantUpdate(MacMessage message, MacStructure mac) MutableIdentifierCollection mic2 = getIdentifierCollectionForUser(cgu.getPatchgroupB(), message.getTimestamp()); mic2.update(cgu.getChannelB()); mTrafficChannelManager.processP2ChannelUpdate(cgu.getChannelB(), serviceOptions, mic2, - mac.getOpcode(), message.getTimestamp()); + mac.getOpcode(), message.getTimestamp(), mac.toString()); } } } @@ -1129,6 +1058,22 @@ else if(getTimeslot() == P25P1Message.TIMESLOT_1) } } + @Override + public void setCurrentChannel(IChannelDescriptor channel) + { + if(channel == null) + { + return; + } + + if(channel instanceof APCO25Channel apco25Channel && apco25Channel.getTimeslot() != getTimeslot()) + { + channel = apco25Channel.decorateAs(getTimeslot()); + } + + super.setCurrentChannel(channel); + } + /** * Channel user (ie current user on this channel). * @@ -1152,7 +1097,9 @@ private void processChannelUser(MacMessage message, MacStructure mac) { if(mac instanceof IServiceOptionsProvider sop) { - IChannelDescriptor currentChannel = mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), sop.getServiceOptions(), mac.getOpcode(), getIdentifierCollection().copyOf(), message.getTimestamp(), null); + IChannelDescriptor currentChannel = mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), + getTimeslot(), getCurrentChannel(), sop.getServiceOptions(), mac.getOpcode(), + getIdentifierCollection().copyOf(), message.getTimestamp(), null, message.toString()); if(getCurrentChannel() == null) { @@ -1195,9 +1142,14 @@ private void processPushToTalk(MacMessage message, MacStructure mac) if(mac instanceof PushToTalk ptt) { VoiceServiceOptions vso = ptt.isEncrypted() ? VoiceServiceOptions.createEncrypted() : VoiceServiceOptions.createUnencrypted(); - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), vso, + + //First TCM call creates the tracked event and second call starts the call and updates the duration + mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), vso, mac.getOpcode(), getIdentifierCollection().copyOf(), message.getTimestamp(), - ptt.isEncrypted() ? ptt.getEncryptionKey().toString() : null); + ptt.isEncrypted() ? ptt.getEncryptionKey().toString() : null, message.toString()); + + mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp()); + broadcast(new DecoderStateEvent(this, Event.START, ptt.isEncrypted() ? State.ENCRYPTED : State.CALL, getTimeslot())); } } @@ -1256,7 +1208,16 @@ private void processEndPushToTalk(MacMessage message, MacStructure mac) { if(mac instanceof EndPushToTalk) { - closeCurrentCallEvent(true, false); + //No matter what, remove the FROM identifier on end PTT. + getIdentifierCollection().remove(Role.FROM); + + //Only reset the identifiers if the call event is closed out, otherwise we might have a timing issue + //between the control channel and the traffic channel. + if(mTrafficChannelManager.processP2TrafficEndPushToTalk(getCurrentFrequency(), getTimeslot(), + message.getTimestamp(), "END PUSH TO TALK - " + message)) + { + getIdentifierCollection().remove(IdentifierClass.USER); + } if(message.getDataUnitID().isFACCH()) { @@ -1269,8 +1230,6 @@ private void processEndPushToTalk(MacMessage message, MacStructure mac) return; //don't issue a reset state after this. } } - - continueState(State.RESET); } } @@ -1514,7 +1473,7 @@ private void processGPS(MacMessage message, MacStructure structure) .build(); broadcast(decodeEvent); mTrafficChannelManager.broadcast(decodeEvent); - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), gps.getLocation(), + mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), gps.getLocation(), message.getTimestamp()); } } @@ -1543,7 +1502,8 @@ private void processMacRelease(MacMessage message, MacStructure mac) { if(mac instanceof MacRelease mr) { - closeCurrentCallEvent(true, false); + mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), message.getTimestamp(), "MAC RELEASE: " + mac.toString()); + getIdentifierCollection().remove(IdentifierClass.USER); broadcast(message, mac, DecodeEventType.COMMAND, (mr.isForcedPreemption() ? "FORCED " : "") + "CALL PREEMPTION" + (mr.isTalkerPreemption() ? " BY USER" : " BY CONTROLLER")); @@ -1746,7 +1706,7 @@ private void processTalkerAlias(MacMessage message, MacStructure mac) { P25TalkerAliasIdentifier alias = talkerAlias.getAlias(); getIdentifierCollection().update(alias); - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), alias, message.getTimestamp()); + mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), alias, message.getTimestamp()); //Add the alias to the talker alias manager if we know the associated radio Identifier from = getIdentifierCollection().getFromIdentifier(); @@ -1839,50 +1799,6 @@ private void continueState(State state) } - /** - * Updates or creates a current call event. - * - * @param macOpcode for the update - * @param serviceOptions for the call - * @param details of the call (optional) - * @param timestamp of the message indicating a call or continuation - */ - private void updateCurrentCall(MacOpcode macOpcode, ServiceOptions serviceOptions, String details, long timestamp) - { - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), serviceOptions, macOpcode, getIdentifierCollection().copyOf(), timestamp, details); - - if(isEncrypted()) - { - broadcast(new DecoderStateEvent(this, Event.START, State.ENCRYPTED, getTimeslot())); - } - else - { - broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot())); - } - - } - - /** - * Ends/closes the current call event. - * - * @param resetIdentifiers to reset the FROM/TO identifiers (true) or reset just the FROM identifiers (false) - * @param isIdleNull to indicate if the calling trigger is an IDLE/NULL message - */ - private void closeCurrentCallEvent(boolean resetIdentifiers, boolean isIdleNull) - { - mTrafficChannelManager.closeP2CallEvent(getCurrentFrequency(), getTimeslot(), isIdleNull); - - if(resetIdentifiers) - { - getIdentifierCollection().remove(IdentifierClass.USER); - } - else - { - //Only clear the from identifier(s) at this point ... the channel may still be allocated to the TO talkgroup - getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); - } - } - /** * Indicates if the current set of identifiers contains an encryption key indicating that the communication is * encrypted. diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java index 2c6f0028a..8812f52a5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java @@ -20,7 +20,6 @@ import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; @@ -30,11 +29,8 @@ import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.MessageProviderModule; import io.github.dsheirer.module.ProcessingChain; -import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager; import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule; -import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence; -import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID; import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters; import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message; import io.github.dsheirer.preference.UserPreferences; @@ -59,23 +55,15 @@ public class P25P2MessageFramer implements Listener { private final static Logger mLog = LoggerFactory.getLogger(P25P2MessageFramer.class); - private P25P2SuperFrameDetector mSuperFrameDetector; - private boolean mAssemblingMessage = false; - private CorrectedBinaryMessage mBinaryMessage; - private DataUnitID mDataUnitID; - private PDUSequence mPDUSequence; - private int[] mCorrectedNID; - private int mNAC; - private int mStatusSymbolDibitCounter = 0; - private int mTrailingDibitsToSuppress = 0; - private double mBitRate; - private long mCurrentTime = System.currentTimeMillis(); - public P25P2MessageFramer(IPhaseLockedLoop phaseLockedLoop, int bitRate) + /** + * Constructs an instance + * @param phaseLockedLoop for use with the super frame detector + */ + public P25P2MessageFramer(IPhaseLockedLoop phaseLockedLoop) { mSuperFrameDetector = new P25P2SuperFrameDetector(phaseLockedLoop); - mBitRate = bitRate; } /** @@ -109,41 +97,6 @@ public void setSyncDetectListener(ISyncDetectListener listener) mSuperFrameDetector.setSyncDetectListener(listener); } - /** - * Current timestamp or timestamp of incoming message buffers that is continuously updated to as - * close as possible to the bits processed for the expected baud rate. - * - * @return - */ - private long getTimestamp() - { - return mCurrentTime; - } - - /** - * Sets the current time. This should be invoked by an incoming message buffer stream. - * - * @param currentTime - */ - public void setCurrentTime(long currentTime) - { - mCurrentTime = currentTime; - } - - /** - * Updates the current timestamp based on the number of bits processed versus the bit rate per second - * in order to keep an accurate running timestamp to use for timestamped message creation. - * - * @param bitsProcessed thus far - */ - private void updateBitsProcessed(int bitsProcessed) - { - if(bitsProcessed > 0) - { - mCurrentTime += (long)((double)bitsProcessed / mBitRate * 1000.0); - } - } - /** * Registers the listener for messages produced by this message framer * @@ -154,11 +107,6 @@ public void setListener(Listener messageListener) mSuperFrameDetector.setListener(messageListener); } - public P25P2SuperFrameDetector getSuperFrameDetector() - { - return mSuperFrameDetector; - } - /** * Primary method for streaming decoded symbol dibits for message framing. * @@ -170,18 +118,6 @@ public void receive(Dibit dibit) mSuperFrameDetector.receive(dibit); } - private void reset(int bitsProcessed) - { - updateBitsProcessed(bitsProcessed); - mPDUSequence = null; - mBinaryMessage = null; - mAssemblingMessage = false; - mDataUnitID = null; - mNAC = 0; - mSuperFrameDetector.reset(); - mStatusSymbolDibitCounter = 0; - } - /** * Primary method for streaming decoded symbol byte arrays. * @@ -266,7 +202,7 @@ public void accept(Path path) processingChain.start(); - P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, DecoderType.P25_PHASE2.getProtocol().getBitRate()); + P25P2MessageFramer messageFramer = new P25P2MessageFramer(null); messageFramer.setScrambleParameters(scrambleParameters); P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor(); messageFramer.setListener(messageProcessor); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/reference/ServiceOptions.java b/src/main/java/io/github/dsheirer/module/decode/p25/reference/ServiceOptions.java index 6a98aec33..a24faff4d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/reference/ServiceOptions.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/reference/ServiceOptions.java @@ -25,7 +25,7 @@ public abstract class ServiceOptions { protected static final int EMERGENCY_FLAG = 0x80; - protected static final int ENCRYPTION_FLAG = 0x40; + public static final int ENCRYPTION_FLAG = 0x40; protected static final int DUPLEX = 0x20; protected static final int SESSION_MODE = 0x10; protected int mServiceOptions;