From de8cfa24f9228b4d918bf847309538ddfb42ab12 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 25 Oct 2020 04:51:36 -0400 Subject: [PATCH] #959 Updates DMR decode event tracking and Con+ channel grant management. (#978) Co-authored-by: Denny --- .../channelizer/PolyphaseChannelManager.java | 37 +-- .../channel/DMRConfigurationEditor.java | 3 +- .../channel/MPT1327ConfigurationEditor.java | 3 +- .../channel/P25P1ConfigurationEditor.java | 3 +- .../gui/playlist/source/FrequencyEditor.java | 4 +- .../module/decode/dmr/DMRDecoderState.java | 244 +++++++++----- .../decode/dmr/DMRTrafficChannelManager.java | 313 +++++++++++------- .../module/decode/dmr/DecodeConfigDMR.java | 1 + .../decode/dmr/channel/DMRLogicalChannel.java | 1 - .../decode/dmr/message/data/csbk/Opcode.java | 2 - .../module/decode/event/DecodeEvent.java | 23 +- .../module/decode/event/IDecodeEvent.java | 6 + .../decode/mpt1327/DecodeConfigMPT1327.java | 1 + .../p25/phase1/DecodeConfigP25Phase1.java | 3 +- .../rotation/ChannelRotationMonitor.java | 1 + 15 files changed, 408 insertions(+), 237 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 6f0499a7..8d6d8941 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -165,34 +165,25 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { PolyphaseChannelSource channelSource = null; - try + List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + + IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); + + if(outputProcessor != null) { - List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); - IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); - - if(outputProcessor != null) + try { - long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, + mChannelCalculator.getChannelSampleRate(), centerFrequency, channelSpecification); - try - { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - mChannelCalculator.getChannelSampleRate(), centerFrequency, channelSpecification); - - mChannelSources.add(channelSource); - } - catch(FilterDesignException fde) - { - mLog.debug("Couldn't design final output low pass filter for polyphase channel source"); - } + mChannelSources.add(channelSource); + } + catch(FilterDesignException fde) + { + mLog.debug("Couldn't design final output low pass filter for polyphase channel source"); } - } - catch(IllegalArgumentException iae) - { - mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" + - mChannelCalculator.getCenterFrequency() + "] and sample rate [" + - (mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]"); } return channelSource; diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java index aedf30b7..128d6a45 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java @@ -233,7 +233,8 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor { mSourceConfigurationEditor = new FrequencyEditor(getTunerModel(), DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MINIMUM_MS, - DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MAXIMUM_MS); + DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MAXIMUM_MS, + DecodeConfigDMR.CHANNEL_ROTATION_DELAY_DEFAULT_MS); //Add a listener so that we can push change notifications up to this editor mSourceConfigurationEditor.modifiedProperty() diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java index d95411e3..485ff35b 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java @@ -197,7 +197,8 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor { mSourceConfigurationEditor = new FrequencyEditor(getTunerModel(), DecodeConfigMPT1327.CHANNEL_ROTATION_DELAY_MINIMUM_MS, - DecodeConfigMPT1327.CHANNEL_ROTATION_DELAY_MAXIMUM_MS); + DecodeConfigMPT1327.CHANNEL_ROTATION_DELAY_MAXIMUM_MS, + DecodeConfigMPT1327.CHANNEL_ROTATION_DELAY_DEFAULT_MS); //Add a listener so that we can push change notifications up to this editor mSourceConfigurationEditor.modifiedProperty() diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java index f63d26a0..fb6d8c40 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java @@ -193,7 +193,8 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor { mSourceConfigurationEditor = new FrequencyEditor(getTunerModel(), DecodeConfigP25Phase1.CHANNEL_ROTATION_DELAY_MINIMUM_MS, - DecodeConfigP25Phase1.CHANNEL_ROTATION_DELAY_MAXIMUM_MS); + DecodeConfigP25Phase1.CHANNEL_ROTATION_DELAY_MAXIMUM_MS, + DecodeConfigP25Phase1.CHANNEL_ROTATION_DELAY_DEFAULT_MS); //Add a listener so that we can push change notifications up to this editor mSourceConfigurationEditor.modifiedProperty() diff --git a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java index 78cdfae1..70dcaeb3 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java @@ -66,6 +66,7 @@ public class FrequencyEditor extends SourceConfigurationEditor mChannelRotationDelaySpinner; private boolean mAllowMultipleFrequencies = false; + private int mFrequencyRotationDefault = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_DEFAULT; private int mFrequencyRotationMinimum = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM; private int mFrequencyRotationMaximum = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MAXIMUM; @@ -78,12 +79,13 @@ public class FrequencyEditor extends SourceConfigurationEditor mDetectedCallEventsMap = new TreeMap<>(); + private Map mDetectedCallEventsMap = new TreeMap<>(); private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST = new AddChannelRotationActiveStateRequest(State.ACTIVE); /** * Constructs an DMR decoder state with an optional traffic channel manager. * @param channel with configuration details + * @param timeslot for this decoder state (1 or 2) * @param trafficChannelManager for handling traffic channel grants. - * @param configurationMonitor for tracking activity summary */ public DMRDecoderState(Channel channel, int timeslot, DMRTrafficChannelManager trafficChannelManager) { @@ -111,7 +110,7 @@ public class DMRDecoderState extends TimeslotDecoderState mChannel = channel; mTrafficChannelManager = trafficChannelManager; - //The decoder state passes all messages to the network configuration monitor, so we only contruct + //The decoder state passes all messages to the network configuration monitor, so we only construct //the monitor for timeslot 1. if(timeslot == 1) { @@ -181,7 +180,7 @@ public class DMRDecoderState extends TimeslotDecoderState @Override public void receive(IMessage message) { - if(message.isValid() && message.getTimeslot() == getTimeslot()) + if(message.getTimeslot() == getTimeslot()) { if(message instanceof VoiceMessage) { @@ -191,11 +190,11 @@ public class DMRDecoderState extends TimeslotDecoderState { processData((DataMessage)message); } - else if(message instanceof LCMessage) + else if(message.isValid() && message instanceof LCMessage) { processLinkControl((LCMessage)message, false); } - else if(message instanceof DMRPacketMessage) + else if(message.isValid() && message instanceof DMRPacketMessage) { processPacket((DMRPacketMessage)message); } @@ -317,13 +316,16 @@ public class DMRDecoderState extends TimeslotDecoderState /** * Process Data Messages + * + * Note: invalid messages are allowed to pass to this method. Messages are selectively checked for isValid() + * to overcome RAS implementation in certain systems. */ private void processData(DataMessage message) { switch(message.getSlotType().getDataType()) { case CSBK: - if(message instanceof CSBKMessage) + if(message.isValid() && message instanceof CSBKMessage) { processCSBK((CSBKMessage)message); } @@ -602,12 +604,37 @@ public class DMRDecoderState extends TimeslotDecoderState { ChannelGrant dataGrant = (ChannelGrant)csbk; DMRChannel channel = dataGrant.getChannel(); + + IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); + if(hasTrafficChannelManager()) { - mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), - csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + else + { + DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) + { + event = DMRDecodeEvent.builder(csbk.getTimestamp()) + .channel(channel) + .details(csbk.getOpcode().getLabel()) + .eventDescription(DecodeEventType.DATA_CALL.toString()) + .identifiers(mergedIdentifiers) + .timeslot(channel.getTimeslot()) + .build(); + mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + else + { + //Update the ending timestamp for the event and rebroadcast + event.end(csbk.getTimestamp()); + } + + broadcast(event); } - processCallDetection(dataGrant.getChannel(), dataGrant.getIdentifiers(), dataGrant.getTimestamp(), DecodeEventType.DATA_CALL); } break; case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: @@ -616,12 +643,37 @@ public class DMRDecoderState extends TimeslotDecoderState { ChannelGrant tgGrant = (ChannelGrant)csbk; DMRChannel channel = tgGrant.getChannel(); + + IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); + if(hasTrafficChannelManager()) { - mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); } - processCallDetection(tgGrant.getChannel(), tgGrant.getIdentifiers(), tgGrant.getTimestamp(), DecodeEventType.CALL_GROUP); + else + { + DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) + { + event = DMRDecodeEvent.builder(csbk.getTimestamp()) + .channel(channel) + .details(csbk.getOpcode().getLabel()) + .eventDescription(DecodeEventType.CALL_GROUP.toString()) + .identifiers(mergedIdentifiers) + .timeslot(channel.getTimeslot()) + .build(); + mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + else + { + //Update the ending timestamp for the event and rebroadcast + event.end(csbk.getTimestamp()); + } + + broadcast(event); + } } break; case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: @@ -630,13 +682,36 @@ public class DMRDecoderState extends TimeslotDecoderState { ChannelGrant channelGrant = (ChannelGrant)csbk; DMRChannel channel = channelGrant.getChannel(); + IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); + if(hasTrafficChannelManager()) { - mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), + mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); } - processCallDetection(channelGrant.getChannel(), channelGrant.getIdentifiers(), - channelGrant.getTimestamp(), DecodeEventType.CALL_UNIT_TO_UNIT); + else + { + DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) + { + event = DMRDecodeEvent.builder(csbk.getTimestamp()) + .channel(channel) + .details(csbk.getOpcode().getLabel()) + .eventDescription(DecodeEventType.CALL_UNIT_TO_UNIT.toString()) + .identifiers(mergedIdentifiers) + .timeslot(channel.getTimeslot()) + .build(); + mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + else + { + //Update the ending timestamp for the event and rebroadcast + event.end(csbk.getTimestamp()); + } + + broadcast(event); + } } break; case MOTOROLA_CAPMAX_ALOHA: @@ -664,13 +739,38 @@ public class DMRDecoderState extends TimeslotDecoderState if(csbk instanceof ConnectPlusDataChannelGrant) { ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk; + DMRChannel channel = cpdcg.getChannel(); + + IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); + if(hasTrafficChannelManager()) { - mTrafficChannelManager.processChannelGrant(cpdcg.getChannel(), - getMergedIdentifierCollection(csbk.getIdentifiers()), csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + else + { + DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) + { + event = DMRDecodeEvent.builder(csbk.getTimestamp()) + .channel(channel) + .details(csbk.getOpcode().getLabel()) + .eventDescription(DecodeEventType.DATA_CALL.toString()) + .identifiers(mergedIdentifiers) + .timeslot(channel.getTimeslot()) + .build(); + mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); + } + else + { + //Update the ending timestamp for the event and rebroadcast + event.end(csbk.getTimestamp()); + } + + broadcast(event); } - processCallDetection(cpdcg.getChannel(), cpdcg.getIdentifiers(), cpdcg.getTimestamp(), - DecodeEventType.DATA_CALL); } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; @@ -697,14 +797,39 @@ public class DMRDecoderState extends TimeslotDecoderState case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: if(csbk instanceof ConnectPlusVoiceChannelUser) { - DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel(); + ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk; + DMRChannel channel = cpvcu.getChannel(); + + IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); + if(hasTrafficChannelManager()) { - mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()), - csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); + mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + else + { + DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); + + if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers())) + { + detectedEvent = DMRDecodeEvent.builder(csbk.getTimestamp()) + .channel(channel) + .details(csbk.getOpcode().getLabel()) + .eventDescription(DecodeEventType.CALL_GROUP.toString()) + .identifiers(mergedIdentifiers) + .timeslot(channel.getTimeslot()) + .build(); + mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), detectedEvent); + } + else + { + //Update the ending timestamp for the event and rebroadcast + detectedEvent.end(csbk.getTimestamp()); + } + + broadcast(detectedEvent); } - processCallDetection(channel, csbk.getIdentifiers(), csbk.getTimestamp(), - DecodeEventType.CALL_GROUP); } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; @@ -718,9 +843,6 @@ public class DMRDecoderState extends TimeslotDecoderState broadcast(affiliateEvent); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; - case STANDARD_MOVE_TSCC: - - default: broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); break; @@ -742,68 +864,22 @@ public class DMRDecoderState extends TimeslotDecoderState } /** - * Process Call Detections. + * Indicates if the event is a stale event, meaning that the event is null, or the event start exceeds the max + * valid call duration threshold, or if the event identifiers don't match the current identifiers. * - * Note: once full support for Connect+ trunk tracking is implemented with the DMRTrafficChannelManager, this will - * have to be modified to detect if the trunked channel was allocated or if the call was simply detected. This - * would be contingent on having a map of Logical Slot Numbers to frequency for the traffic channel manager to make - * an allocation. - * - * @param channel for the call event - * @param identifiers for the call event - * @param timestamp of the event or update + * @param event to check for staleness + * @param timestamp to check the event against + * @param currentIdentifiers to compare against the event + * @return true if the event is stale. */ - private void processCallDetection(DMRChannel channel, List identifiers, long timestamp, - DecodeEventType eventType) + private boolean isStale(DecodeEvent event, long timestamp, List currentIdentifiers) { - //Check to see if there is a current call event to see if the detected call event is actually for this timeslot - //and we can then identify the LSN for this timeslot - if(mCurrentFrequency == 0 && mCurrentCallEvent != null && - isSameCall(mCurrentCallEvent.getIdentifierCollection(), identifiers)) + if(event == null || (timestamp - event.getTimeStart() > MAX_VALID_CALL_DURATION_MS)) { - mCurrentFrequency = channel.getDownlinkFrequency(); - - if(mNetworkConfigurationMonitor != null) - { - mNetworkConfigurationMonitor.setCurrentChannel(channel); - } + return true; } - DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); - - if(event == null) - { - event = DMRDecodeEvent.builder(timestamp) - .timeslot(getTimeslot()) - .identifiers(getMergedIdentifierCollection(identifiers)) - .eventDescription(eventType.toString()) - .build(); -// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); - } - else - { - if(event.getIdentifierCollection() != null && - isSameCall(event.getIdentifierCollection(), identifiers) && - FastMath.abs(timestamp - event.getTimeStart()) < MAX_VALID_CALL_DURATION_MS) - { - event.update(timestamp); - } - else - { - event = DMRDecodeEvent.builder(timestamp) - .timeslot(getTimeslot()) - .identifiers(getMergedIdentifierCollection(identifiers)) - .eventDescription(DecodeEventType.CALL_DETECT.toString()) - .build(); -// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); - } - } - - //Only broadcast the call detect event if it doesn't match the current logical slot number -// if(mCurrentLSN == null || mCurrentLSN != channel.getLogicalSlotNumber()) -// { -// broadcast(event); -// } + return !isSameCall(event.getIdentifierCollection(), currentIdentifiers); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java index 4a6bf168..74d7f450 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -41,6 +41,7 @@ import io.github.dsheirer.message.MessageHistoryResponse; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; import io.github.dsheirer.module.decode.dmr.event.DMRChannelGrantEvent; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.DecodeEventHistory; @@ -90,8 +91,12 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I IChannelEventProvider { private final static Logger mLog = LoggerFactory.getLogger(DMRTrafficChannelManager.class); - public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; + public static final String CHANNEL_START_REJECTED = " REJECTED - NO TUNER"; + public static final String DATA_CALL_IGNORED = "DATA CALL IGNORED"; + public static final String IGNORED = " - Ignored"; public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED"; + public static final String NO_FREQUENCY = "NO FREQUENCY - CHECK LSN CHANNEL MAP"; + public static final long EVENT_TIME_STALE_THRESHOLD = 5000; //5 seconds private Queue mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>(); private List mManagedTrafficChannels; @@ -325,7 +330,19 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn); - if(event != null && isSameCall(identifierCollection, event.getIdentifierCollection())) + if(isStale(event, timestamp, identifierCollection)) //Create new event + { + event = DMRChannelGrantEvent.channelGrantBuilder(timestamp) + .channel(channel) + .eventDescription(getEventType(opcode, identifierCollection, encrypted).toString()) + .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) + .identifiers(identifierCollection) + .build(); + + mLSNGrantEventMap.put(lsn, event); + + } + else //Update current event { Identifier from = getIdentifier(identifierCollection, Role.FROM); @@ -336,115 +353,98 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I { event.end(timestamp); - DMRChannelGrantEvent continuationGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) + event = DMRChannelGrantEvent.channelGrantBuilder(timestamp) .channel(channel) - .eventDescription(getEventType(opcode, event.isEncrypted()).toString() + " - Continue") + .eventDescription(getEventType(opcode, identifierCollection, encrypted).toString() + " - Continue") .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) .identifiers(identifierCollection) .build(); - mLSNGrantEventMap.put(lsn, continuationGrantEvent); - broadcast(continuationGrantEvent); + mLSNGrantEventMap.put(lsn, event); + broadcast(event); } } //update the ending timestamp so that the duration value is correctly calculated event.update(timestamp); - broadcast(event); - - //Even though we have an event, the initial channel grant may have been rejected. Check to see if there - //is a traffic channel allocated. If not, allocate one and update the event description. - long frequency = channel.getDownlinkFrequency(); - - if(frequency > 0 && frequency != mCurrentControlFrequency && - !mAllocatedTrafficChannelFrequencyMap.containsKey(channel.getDownlinkFrequency()) && - !(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode())) - { - Channel trafficChannel = mAvailableTrafficChannelQueue.poll(); - - if(trafficChannel != null) - { - event.setEventDescription(getEventType(opcode, event.isEncrypted()).toString()); - event.setDetails("CHANNEL GRANT " + (encrypted ? " ENCRYPTED" : "")); - event.setChannelDescriptor(channel); - broadcast(event); - - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel); - getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, identifierCollection)); - } - else - { - String description = (event.getEventDescription() != null ? event.getEventDescription() : "") + - " - MAX TRAFFIC CHANNELS EXCEEDED"; - event.setEventDescription(description); - } - } - - return; } - if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode()) - { - DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) - .encrypted(encrypted) - .channel(channel) - .eventDescription(getEventType(opcode, encrypted).toString() + " - Ignored") - .details("DATA CALL IGNORED") - .identifiers(identifierCollection) - .build(); + broadcast(event); - mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent); - broadcast(channelGrantEvent); - return; - } - - DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp) - .encrypted(encrypted) - .channel(channel) - .eventDescription(getEventType(opcode, encrypted).toString()) - .details("CHANNEL GRANT") - .identifiers(identifierCollection) - .build(); - - mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent); - - //Allocate a traffic channel for the downlink frequency if one isn't already allocated - //NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future + //Even though we have an event, the initial or continuation channel grant may have been rejected. Check to see + // if there is a traffic channel allocated. If not, allocate one and update the event description. long frequency = channel.getDownlinkFrequency(); - if(frequency > 0 && frequency != mCurrentControlFrequency && - !mAllocatedTrafficChannelFrequencyMap.containsKey(frequency)) + if(frequency == 0) { + if(event.getDetails() == null) + { + event.setDetails(NO_FREQUENCY); + } + else if(!event.getDetails().endsWith(NO_FREQUENCY)) + { + event.setDetails(event.getDetails() + " - " + NO_FREQUENCY); + } + + return; + } + + if(frequency != mCurrentControlFrequency && !mAllocatedTrafficChannelFrequencyMap.containsKey(frequency)) + { + if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode()) + { + if(event.getEventDescription() == null) + { + event.setEventDescription(getEventType(opcode, identifierCollection, encrypted) + IGNORED); + } + else if(!event.getEventDescription().endsWith(IGNORED)) + { + event.setEventDescription(event.getEventDescription() + IGNORED); + } + + if(event.getDetails() == null) + { + event.setDetails(DATA_CALL_IGNORED); + } + else if(!event.getDetails().endsWith(DATA_CALL_IGNORED)) + { + event.setDetails(event.getDetails() + " - " + DATA_CALL_IGNORED); + } + + broadcast(event); + return; + } + Channel trafficChannel = mAvailableTrafficChannelQueue.poll(); - if(trafficChannel == null) - { - String details = (channelGrantEvent.getDetails() != null ? channelGrantEvent.getDetails() : "") + - " " + MAX_TRAFFIC_CHANNELS_EXCEEDED; - channelGrantEvent.setDetails(details); - channelGrantEvent.setEventDescription(channelGrantEvent.getEventDescription() + " - Ignored"); - } - else + if(trafficChannel != null) { SourceConfigTuner sourceConfig = new SourceConfigTuner(); sourceConfig.setFrequency(frequency); trafficChannel.setSourceConfiguration(sourceConfig); mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel); - broadcast(ChannelEvent.requestEnable(trafficChannel)); + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, + identifierCollection)); + } + else + { + if(event.getDetails() == null) + { + event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED); + } + else if(!event.getDetails().endsWith(MAX_TRAFFIC_CHANNELS_EXCEEDED)) + { + event.setDetails(event.getDetails() + " - " + MAX_TRAFFIC_CHANNELS_EXCEEDED); + } } } - - broadcast(channelGrantEvent); } /** * Creates a call event type description for the specified opcode and service options */ - private DecodeEventType getEventType(Opcode opcode, boolean encrypted) + private DecodeEventType getEventType(Opcode opcode, IdentifierCollection identifierCollection, boolean encrypted) { DecodeEventType type = null; @@ -452,10 +452,22 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I { case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: - case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; break; + case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: + Identifier to = identifierCollection.getToIdentifier(); + + if(to instanceof DMRRadio) + { + type = encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; + } + else + { + type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; + } + break; + case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: type = encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT; @@ -535,6 +547,31 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I return Objects.equals(toIdentifier1, toIdentifier2); } + /** + * Indicates if the event is a stale event, meaning that the event is null, or the event start exceeds the max + * valid call duration threshold, or if the event identifiers don't match the current identifiers. + * + * @param event to check for staleness + * @param timestamp to check the event against + * @param currentIdentifiers to compare against the event + * @return true if the event is stale. + */ + private boolean isStale(DecodeEvent event, long timestamp, IdentifierCollection currentIdentifiers) + { + if(event == null) + { + return true; + } + + if(timestamp - event.getTimeEnd() > EVENT_TIME_STALE_THRESHOLD) + { + return true; + } + + return !isSameCall(event.getIdentifierCollection(), currentIdentifiers); + } + + /** * Retrieves the first identifier with a TO role. * @@ -617,9 +654,8 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I /** * Removes any call events that are associated with the specified frequency. * @param frequency that was removed - * @param description to optionally append to the details for the event */ - private void cleanupCallEvents(long frequency, String description) + private void removeCallEvents(long frequency) { List lsnsToRemove = new ArrayList<>(); @@ -633,20 +669,35 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I for(Integer lsn: lsnsToRemove) { - DMRChannelGrantEvent event = mLSNGrantEventMap.remove(lsn); + mLSNGrantEventMap.remove(lsn); + } + } + + private void updateCallEventDetails(long frequency, String detailFragment) + { + List lsnsToUpdate = new ArrayList<>(); + + for(Map.Entry entry: mLSNGrantEventMap.entrySet()) + { + if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency) + { + lsnsToUpdate.add(entry.getKey()); + } + } + + for(Integer lsn: lsnsToUpdate) + { + DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn); if(event != null) { - if(description != null) + if(event.getDetails() == null) { - if(event.getEventDescription() != null) - { - event.setEventDescription(event.getEventDescription() + " - " + description); - } - else - { - event.setEventDescription(description); - } + event.setDetails(detailFragment); + } + else if(!event.getDetails().endsWith(detailFragment)) + { + event.setDetails(event.getDetails() + detailFragment); } broadcast(event); @@ -659,36 +710,64 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I { Channel channel = channelEvent.getChannel(); - if(channel.isTrafficChannel() && (channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_STOP || - channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_START_REJECTED)) + if(channel.isTrafficChannel()) { - String optionalDescription = (channelEvent.getEvent() == Event.NOTIFICATION_PROCESSING_START_REJECTED ? - "Rejected" : null); - - long frequencyToRemove = 0; - - for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + switch(channelEvent.getEvent()) { - if(channel == entry.getValue() && entry.getKey() != null) - { - frequencyToRemove = entry.getKey(); - cleanupCallEvents(entry.getKey(), optionalDescription); + case NOTIFICATION_PROCESSING_STOP: + long frequencyToRemove = 0; + + for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + { + if(channel == entry.getValue() && entry.getKey() != null) + { + frequencyToRemove = entry.getKey(); + removeCallEvents(entry.getKey()); + break; + } + } + + if(frequencyToRemove > 0) + { + mAllocatedTrafficChannelFrequencyMap.remove(frequencyToRemove); + + //Unlock the frequency in the channel rotation monitor + getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToRemove)); + } + + //Add the traffic channel back to the queue to be reused + if(!mAvailableTrafficChannelQueue.contains(channel)) + { + mAvailableTrafficChannelQueue.add(channel); + } break; - } - } + case NOTIFICATION_PROCESSING_START_REJECTED: + long frequencyToUpdate = 0; - if(frequencyToRemove > 0) - { - mAllocatedTrafficChannelFrequencyMap.remove(frequencyToRemove); + for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + { + if(channel == entry.getValue() && entry.getKey() != null) + { + frequencyToUpdate = entry.getKey(); + updateCallEventDetails(entry.getKey(), CHANNEL_START_REJECTED); + break; + } + } - //Unlock the frequency in the channel rotation monitor - getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToRemove)); - } + if(frequencyToUpdate > 0) + { + mAllocatedTrafficChannelFrequencyMap.remove(frequencyToUpdate); - //Add the traffic channel back to the queue to be reused - if(!mAvailableTrafficChannelQueue.contains(channel)) - { - mAvailableTrafficChannelQueue.add(channel); + //Unlock the frequency in the channel rotation monitor + getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(frequencyToUpdate)); + } + + //Add the traffic channel back to the queue to be reused + if(!mAvailableTrafficChannelQueue.contains(channel)) + { + mAvailableTrafficChannelQueue.add(channel); + } + break; } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java index b69d8e5c..c82790f1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java @@ -35,6 +35,7 @@ import java.util.List; public class DecodeConfigDMR extends DecodeConfiguration { public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 200; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500; public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000; private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; private boolean mIgnoreDataCalls = true; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java index 79fd764a..9117b954 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLogicalChannel.java @@ -88,7 +88,6 @@ public class DMRLogicalChannel extends DMRChannel StringBuilder sb = new StringBuilder(); sb.append("LSN:").append(getLogicalSlotNumber()); sb.append(" LCN:").append(getValue()); - sb.append(" TS:").append(getTimeslot()); return sb.toString(); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java index d1b1e9c0..5c898680 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java @@ -79,8 +79,6 @@ public enum Opcode MOTOROLA_CONPLUS_DATA_WINDOW_ANNOUNCEMENT(Vendor.MOTOROLA_CONNECT_PLUS, 28, "ENHANCED DATA REVERT WINDOW ANNOUNCEMENT"), MOTOROLA_CONPLUS_DATA_WINDOW_GRANT(Vendor.MOTOROLA_CONNECT_PLUS, 29, "ENHANCED DATA REVERT WINDOW GRANT"), - //TODO: Capacity Max (Tier III) also supports Enhanced Data Revert channels with windowing ... they either - //TODO: use CSBKO 41 and 42, or they have a different pair of messages. MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "ALOHA"), MOTOROLA_CAPPLUS_CALL_ALERT(Vendor.MOTOROLA_CAPACITY_PLUS, 31, "CALL ALERT"), diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java index 831ca54c..72cc3920 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java @@ -34,7 +34,7 @@ import io.github.dsheirer.protocol.Protocol; public class DecodeEvent implements IDecodeEvent { private long mTimeStart; - private long mDuration; + private long mTimeEnd; private String mEventDescription; private IdentifierCollection mIdentifierCollection; private IChannelDescriptor mChannelDescriptor; @@ -66,7 +66,7 @@ public class DecodeEvent implements IDecodeEvent { if(timestamp > mTimeStart) { - mDuration = timestamp - mTimeStart; + mTimeEnd = timestamp; } } @@ -87,13 +87,26 @@ public class DecodeEvent implements IDecodeEvent return mTimeStart; } + /** + * Event end in milliseconds + */ + public long getTimeEnd() + { + return mTimeEnd; + } + /** * Event duration in milliseconds */ @Override public long getDuration() { - return mDuration; + if(mTimeEnd < mTimeStart) + { + return 0; + } + + return mTimeEnd - mTimeStart; } /** @@ -101,7 +114,7 @@ public class DecodeEvent implements IDecodeEvent */ public void setDuration(long duration) { - mDuration = duration; + mTimeEnd = mTimeStart + duration; } /** @@ -229,7 +242,7 @@ public class DecodeEvent implements IDecodeEvent { sb.append(" IDS:").append(Joiner.on(",").join(mIdentifierCollection.getIdentifiers())); } - sb.append(" DURATION:").append(mDuration); + sb.append(" DURATION:").append(getDuration()); sb.append(" CHANNEL:").append(mChannelDescriptor); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/event/IDecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/event/IDecodeEvent.java index 66551c44..12a3703f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/IDecodeEvent.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/IDecodeEvent.java @@ -38,6 +38,12 @@ public interface IDecodeEvent */ long getTimeStart(); + /** + * Event end time + * @return timestamp in milliseconds + */ + long getTimeEnd(); + /** * Event duration. * diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java index 2dcf3056..a13d4265 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/DecodeConfigMPT1327.java @@ -31,6 +31,7 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification; public class DecodeConfigMPT1327 extends DecodeConfiguration implements WithCallTimeout { public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 500; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500; public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000; private String mChannelMapName; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/DecodeConfigP25Phase1.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/DecodeConfigP25Phase1.java index 66e1c805..bf94144a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/DecodeConfigP25Phase1.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/DecodeConfigP25Phase1.java @@ -30,7 +30,8 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification; public class DecodeConfigP25Phase1 extends DecodeConfiguration { - public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 500; + public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 400; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500; public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000; private P25P1Decoder.Modulation mModulation = P25P1Decoder.Modulation.C4FM; diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java index fa4f2547..b2eae315 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java @@ -46,6 +46,7 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid Listener { public static final int CHANNEL_ROTATION_DELAY_MINIMUM = 200; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT = 500; public static final int CHANNEL_ROTATION_DELAY_MAXIMUM = 2000; private final static Logger mLog = LoggerFactory.getLogger(ChannelRotationMonitor.class);