From 686bddf7d58899800ecb8011aafd6ebdfe3f7a21 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 21 Jan 2024 07:47:37 -0500 Subject: [PATCH] #1799 DMR Cap+ & CapMax now track events correctly. Updated Cap+ events to include channel descriptor and frequencies. Corrected Capacity Max talker aliases to combine base and continuation alias fragments into a unified alias identifier. (#1801) Critical: resolved a thread deadlock issue when the DMR multi-frequency channel rotation thread deadlocks against the UI thread when accessing the current channel's frequency from the frequency controller. This thread deadlock may be the source of multiple users reporting OutOfMemory crashes when running any DMR channel with multiple control frequencies where the decoder was rolling through each frequency checking for activity. Co-authored-by: Dennis Sheirer --- .../channel/ChannelProcessingManager.java | 4 +- .../io/github/dsheirer/identifier/Form.java | 3 +- .../alias/DmrTalkerAlias2Identifier.java | 54 ---- .../alias/TalkerAlias2Identifier.java | 46 ---- .../module/decode/DecoderFactory.java | 46 +++- .../module/decode/dmr/DMRDecoderState.java | 112 +++++++- .../decode/dmr/DMRMessageProcessor.java | 58 ++-- .../decode/dmr/DMRTrafficChannelManager.java | 7 +- .../csbk/motorola/CapacityPlusSiteStatus.java | 260 +++++++++++------- .../CapacityMaxTalkerAliasContinuation.java | 28 +- .../source/tuner/TunerController.java | 16 +- 11 files changed, 366 insertions(+), 268 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAlias2Identifier.java delete mode 100644 src/main/java/io/github/dsheirer/identifier/alias/TalkerAlias2Identifier.java diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index c090b36ec..e451c9fec 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.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 @@ -442,7 +442,7 @@ else if(request.hasChildDecodeEventHistory()) /* Processing Modules */ List modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences, - request.getTrafficChannelManager()); + request.getTrafficChannelManager(), request.getChannelDescriptor()); processingChain.addModules(modules); //Post preload data from the request to the event bus. Modules that can handle preload data will annotate diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java index f6c5a9ef4..dcc293927 100644 --- a/src/main/java/io/github/dsheirer/identifier/Form.java +++ b/src/main/java/io/github/dsheirer/identifier/Form.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-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 @@ -54,7 +54,6 @@ public enum Form STATE, SYSTEM, TALKER_ALIAS, - TALKER_ALIAS_2, TALKGROUP, TELEPHONE_NUMBER, TONE, diff --git a/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAlias2Identifier.java b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAlias2Identifier.java deleted file mode 100644 index f689dd904..000000000 --- a/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAlias2Identifier.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * **************************************************************************** - */ - -package io.github.dsheirer.identifier.alias; - -import io.github.dsheirer.protocol.Protocol; - -/** - * DMR Talker alias (alternate/additional) value provided by the network for the current talker (ie FROM). - */ -public class DmrTalkerAlias2Identifier extends TalkerAlias2Identifier -{ - /** - * Constructs an instance. - * @param value of the talker alias - */ - public DmrTalkerAlias2Identifier(String value) - { - super(value); - } - - @Override - public boolean isValid() - { - return getValue() != null && !getValue().isEmpty(); - } - - @Override - public Protocol getProtocol() - { - return Protocol.DMR; - } - - public static DmrTalkerAlias2Identifier create(String value) - { - return new DmrTalkerAlias2Identifier(value); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/alias/TalkerAlias2Identifier.java b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAlias2Identifier.java deleted file mode 100644 index 5d52285ea..000000000 --- a/src/main/java/io/github/dsheirer/identifier/alias/TalkerAlias2Identifier.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * **************************************************************************** - */ - -package io.github.dsheirer.identifier.alias; - -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.string.StringIdentifier; - -/** - * Talker alias (alternate/second) value provided by the network for the current talker (ie FROM). - */ -public abstract class TalkerAlias2Identifier extends StringIdentifier -{ - /** - * Constructs an instance. - * @param value of the string/alias identifier - */ - public TalkerAlias2Identifier(String value) - { - super(value, IdentifierClass.USER, Form.TALKER_ALIAS_2, Role.FROM); - } - - @Override - public boolean isValid() - { - return getValue() != null && !getValue().isEmpty(); - } -} 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 da388ee4d..3b7c392b1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ import io.github.dsheirer.alias.action.AliasActionManager; import io.github.dsheirer.audio.AbstractAudioModule; import io.github.dsheirer.audio.AudioModule; +import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.channel.state.State; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.Channel.ChannelType; @@ -48,7 +49,8 @@ import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; -import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLsn; +import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn; import io.github.dsheirer.module.decode.dmr.message.filter.DmrMessageFilterSet; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder; @@ -123,9 +125,11 @@ public class DecoderFactory * @return list of configured decoders */ public static List getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel, - UserPreferences userPreferences, TrafficChannelManager trafficChannelManager) + UserPreferences userPreferences, TrafficChannelManager trafficChannelManager, + IChannelDescriptor channelDescriptor) { - List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences, trafficChannelManager); + List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences, + trafficChannelManager, channelDescriptor); modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration())); return modules; } @@ -138,10 +142,12 @@ public static List getModules(ChannelMapModel channelMapModel, Channel c * @param aliasModel for alias lookups * @param userPreferences instance * @param trafficChannelManager optional traffic channel manager to use + * @param channelDescriptor to preload into the decoder state as the current channel. * @return list of modules to use for a processing chain */ public static List getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel, - UserPreferences userPreferences, TrafficChannelManager trafficChannelManager) + UserPreferences userPreferences, TrafficChannelManager trafficChannelManager, + IChannelDescriptor channelDescriptor) { List modules = new ArrayList(); @@ -160,7 +166,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch break; case DMR: processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig, - trafficChannelManager); + trafficChannelManager, channelDescriptor); break; case NBFM: processNBFM(channel, modules, aliasList, decodeConfig); @@ -413,7 +419,7 @@ private static void processAM(Channel channel, List modules, AliasList a */ private static void processDMR(Channel channel, UserPreferences userPreferences, List modules, AliasList aliasList, DecodeConfigDMR decodeConfig, - TrafficChannelManager trafficChannelManager) + TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor) { modules.add(new DMRDecoder(decodeConfig)); @@ -437,6 +443,32 @@ private static void processDMR(Channel channel, UserPreferences userPreferences, DMRDecoderState state1 = new DMRDecoderState(channel, 1, dmrTrafficChannelManager); DMRDecoderState state2 = new DMRDecoderState(channel, 2, dmrTrafficChannelManager); + //Register the states with each other so that they can pass Cap+ site status messaging to resolve current channel + state1.setSisterDecoderState(state2); + state2.setSisterDecoderState(state1); + + //If an LSN is provided, apply it to both of the decoder states. + if(channelDescriptor instanceof DMRLsn lsn) + { + //If this is a REST descriptor, change it to a standard LSN descriptor. + if(channelDescriptor instanceof DmrRestLsn rest) + { + lsn = new DMRLsn(rest.getLsn()); + lsn.setTimeslotFrequency(rest.getTimeslotFrequency()); + } + + if(lsn.getTimeslot() == 1) + { + state1.setCurrentChannel(lsn); + state2.setCurrentChannel(lsn.getSisterTimeslot()); + } + else + { + state1.setCurrentChannel(lsn.getSisterTimeslot()); + state2.setCurrentChannel(lsn); + } + } + if(decodeConfig.hasChannelGrantEvent()) { DecodeEvent event = decodeConfig.getChannelGrantEvent(); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index 88e09f5eb..0fbdcabca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.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 @@ -33,19 +33,23 @@ import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.log.LoggingSuppressor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; +import io.github.dsheirer.module.decode.dmr.channel.DMRLsn; import io.github.dsheirer.module.decode.dmr.event.DMRDecodeEvent; import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup; import io.github.dsheirer.module.decode.dmr.message.DMRMessage; import io.github.dsheirer.module.decode.dmr.message.data.DataMessage; import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus; +import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAdvantageModeVoiceChannelUpdate; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxOpenModeVoiceChannelUpdate; -import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAdvantageModeVoiceChannelUpdate; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant; @@ -97,12 +101,12 @@ import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest; import io.github.dsheirer.util.PacketUtil; +import java.util.List; +import java.util.Map; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - /** * Decoder state for an DMR channel. Maintains the call/data/idle state of the channel and produces events by * monitoring the decoded message stream. @@ -120,6 +124,7 @@ public class DMRDecoderState extends TimeslotDecoderState private DecodeEvent mCurrentCallEvent; private long mCurrentFrequency; private boolean mIgnoreCRCChecksums; + private DMRDecoderState mSisterDecoderState; /** * Constructs an DMR decoder state with an optional traffic channel manager. @@ -147,6 +152,54 @@ public DMRDecoderState(Channel channel, int timeslot, DMRTrafficChannelManager t } } + /** + * Registers a reference to the decoder state that is processing the sisten timeslot. If this state is covering + * timeslot 1, then the argument will be the state covering timeslot 2, and vice-versa. + */ + public void setSisterDecoderState(DMRDecoderState state) + { + mSisterDecoderState = state; + } + + /** + * Processes a map of active talkgroups received from the sister timeslot decoder state so that we can recover + * the current channel identifier to enable populating call events with accurate current channel info. + * + * This is used for Capacity Plus systems to recover the current channel. + * + * @param idMap to process + * @param lsnMap to process + */ + public DMRChannel processActiveTalkgroups(Map idMap, Map lsnMap) + { + if(mCurrentCallEvent != null) + { + List toIds = mCurrentCallEvent.getIdentifierCollection().getIdentifiers(Role.TO); + + if(toIds.size() >= 1) + { + Identifier to = toIds.get(0); + + if(to instanceof TalkgroupIdentifier talkgroup) + { + Integer tgValue = talkgroup.getValue(); + + for(Map.Entry entry: idMap.entrySet()) + { + if(tgValue != null && tgValue.equals(entry.getValue().getValue())) + { + DMRLsn lsn = lsnMap.get(entry.getKey()); + setCurrentChannel(lsn); + return lsn; + } + } + } + } + } + + return null; + } + @Override protected void broadcast(IDecodeEvent event) { @@ -788,6 +841,20 @@ private void processCSBK(CSBKMessage csbk) //Update state and rest channel updateRestChannel(cpss.getRestChannel()); + + //If the sister timeslot hasn't identified the current channel, attempt to identify the channel + //from the current call activity map. + if(cpss.hasVoiceTalkgroups() && mSisterDecoderState != null && mSisterDecoderState.getCurrentChannel() == null) + { + DMRChannel sisterChannel = mSisterDecoderState + .processActiveTalkgroups(cpss.getActiveIdentifierMap(), cpss.getActiveLsnMap()); + + //If the returned channel is non-null, set our own channel + if(getCurrentChannel() == null && sisterChannel != null) + { + setCurrentChannel(sisterChannel.getSisterTimeslot()); + } + } } break; case MOTOROLA_CONPLUS_NEIGHBOR_REPORT: @@ -1098,7 +1165,22 @@ private void processLinkControl(LCMessage message, boolean isTerminator) case FULL_CAPACITY_MAX_TALKER_ALIAS: if(message instanceof CapacityMaxTalkerAlias alias) { - getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + //If we have a talker alias identifier, append this value. + Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM); + + if(existing instanceof DmrTalkerAliasIdentifier talkerAlias && + !talkerAlias.equals(alias.getTalkerAliasIdentifier()) && + !talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue())) + { + //Concatenate the existing talker alias fragment with the base alias value. + DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier + .create(alias.getTalkerAliasIdentifier().getValue() + talkerAlias.getValue()); + getIdentifierCollection().update(updated); + } + else + { + getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + } if(mCurrentCallEvent != null) { @@ -1109,7 +1191,23 @@ private void processLinkControl(LCMessage message, boolean isTerminator) case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: if(message instanceof CapacityMaxTalkerAliasContinuation alias) { - getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + //If we have a talker alias identifier, append this value. + Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM); + + if(existing instanceof DmrTalkerAliasIdentifier talkerAlias && + !talkerAlias.equals(alias.getTalkerAliasIdentifier()) && + !talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue())) + { + //Concatenate the existing talker alias value with the updated continuation fragment. + DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier.create(talkerAlias.getValue() + + alias.getTalkerAliasIdentifier().getValue()); + getIdentifierCollection().update(updated); + } + else + { + //Temporarily place the continuation fragment alias into the identifier collection. + getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + } if(mCurrentCallEvent != null) { @@ -1335,6 +1433,8 @@ private void closeCurrentCallEvent(long timestamp) broadcast(mCurrentCallEvent); mCurrentCallEvent = null; } + + getIdentifierCollection().remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java index fb29ca5e7..fc19560eb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.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 @@ -49,13 +49,12 @@ import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceSuperFrameProcessor; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Processes DMR messages and performs re-assembly of link control fragments @@ -159,30 +158,11 @@ else if(message instanceof DMRBurst dmrBurst && isValid(dmrBurst)) //Process data messages carrying a link control payload so that the LC payload can be processed/enriched if(message instanceof DataMessageWithLinkControl linkControlCarrier) { - receive(linkControlCarrier.getLCMessage()); + enrich(linkControlCarrier.getLCMessage()); } //Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings - if(message instanceof ITimeslotFrequencyReceiver) - { - ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message; - int[] lcns = receiver.getLogicalChannelNumbers(); - - List timeslotFrequencies = new ArrayList<>(); - - for(int lcn: lcns) - { - if(mTimeslotFrequencyMap.containsKey(lcn)) - { - timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn)); - } - } - - if(!timeslotFrequencies.isEmpty()) - { - receiver.apply(timeslotFrequencies); - } - } + enrich(message); //Now that the message has been (potentially) enriched, dispatch it to the modules dispatch(message); @@ -278,6 +258,34 @@ else if((message instanceof IDLEMessage || message instanceof Aloha || message i } } + /** + * Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings + * @param message to be enriched + */ + private void enrich(IMessage message) + { + if(message instanceof ITimeslotFrequencyReceiver) + { + ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message; + int[] lcns = receiver.getLogicalChannelNumbers(); + + List timeslotFrequencies = new ArrayList<>(); + + for(int lcn: lcns) + { + if(mTimeslotFrequencyMap.containsKey(lcn)) + { + timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn)); + } + } + + if(!timeslotFrequencies.isEmpty()) + { + receiver.apply(timeslotFrequencies); + } + } + } + /** * Dispatches the non-null message to the registered listener */ 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 35bd45eeb..c620952c8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-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 @@ -55,7 +55,6 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; import io.github.dsheirer.source.tuner.channel.rotation.DisableChannelRotationMonitorRequest; import io.github.dsheirer.source.tuner.channel.rotation.FrequencyLockChangeRequest; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -65,7 +64,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -267,7 +265,8 @@ public void convertToTrafficChannel(Channel channel, long currentFrequency, ICha //Dispatch request to persistently start the original channel with the rest channel frequency and reuse //this traffic channel manager in the new processing chain. - ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(channel, this); + ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(channel, restChannel, + null, this); request.setPersistentAttempt(true); request.setChildDecodeEventHistory(mTransientDecodeEventHistory); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSiteStatus.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSiteStatus.java index 6a7c7651d..bef140fdf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSiteStatus.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityPlusSiteStatus.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 @@ -21,15 +21,25 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRLsn; import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup; import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.data.SlotType; import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Capacity+ Site Status CSBKO=62 Message @@ -43,6 +53,8 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq private static final int[] LSN_VOICE_BITMAP = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; private static final int LSN_1_8_BITMAP_START = 24; + private Map mActiveIdentifierMap; + private Map mActiveLsnMap; private DmrRestLsn mRestChannel; private List mIdentifiers; @@ -96,173 +108,203 @@ public String toString() return sb.toString(); } - /** - * Indicates if this message has voice talkgroup activity. - * - * @return true if voice activity. - */ - private boolean hasVoiceTalkgroups() - { - return getSegmentIndicator().isFirst() && getMessage().getInt(LSN_VOICE_BITMAP) > 0; - } - /** * Extracts the call activity from the first or first/last fragment. * @return description of call activity. */ private String getActivityFragments() { - if(getMessage().get(48) && getMessage().get(56)) + StringBuilder sb = new StringBuilder(); + + if(getActiveIdentifierMap().isEmpty()) { - int a = 0; //debug + sb.append("IDLE LSN: 1-16"); } - int[] talkgroups = new int[17]; - int[] radios = new int[17]; - boolean hasIdentifier = false; - - int pointer = LSN_1_8_BITMAP_START; - - //Process voice LSNs 1-8 - if(getMessage().getInt(BYTE, pointer) > 0) + else { - int lowLsnBitmap = pointer; - pointer += 8; + sb.append("ACTIVE LSN"); - for(int x = lowLsnBitmap; x < (lowLsnBitmap + 8); x++) + for(int x = 1; x < 17; x++) { - int lsn = x - lowLsnBitmap + 1; + sb.append(" ").append(x); - if(getMessage().get(x)) + if(mActiveIdentifierMap.containsKey(x)) { - hasIdentifier = true; + Identifier id = mActiveIdentifierMap.get(x); - if(pointer <= 72) + if(id instanceof TalkgroupIdentifier talkgroup) { - talkgroups[lsn] = getMessage().getInt(BYTE, pointer); - pointer += 8; + if(talkgroup.getValue() > 0) + { + sb.append(":(T)").append(talkgroup.getValue()); //Active talkgroup ID for channel. + } + else + { + sb.append(":(T)A"); //Talkgroup active on channel but identifier is in a continuation message + } } - else + else if(id instanceof RadioIdentifier radio) { - talkgroups[lsn] = -1; + if(radio.getValue() > 0) + { + sb.append(":(R)").append(radio.getValue()); //Active radio ID for channel + } + else + { + sb.append(":(R)A"); //Radio active on channel but identifier is in a continuation message + } } } + else + { + sb.append(":"); //No radio or talkgroup active on channel + } } } - else + + return sb.toString(); + } + + /** + * Indicates if this message has voice talkgroup activity. + * + * @return true if voice activity. + */ + public boolean hasVoiceTalkgroups() + { + return getSegmentIndicator().isFirst() && getMessage().getInt(LSN_VOICE_BITMAP) > 0; + } + + /** + * Map of active LSNs. + * @return map of active LSNs + */ + public Map getActiveLsnMap() + { + if(mActiveLsnMap == null) { - pointer += 8; + getActiveIdentifierMap(); } - //Process voice LSNs 9 - 16 - if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0) + return mActiveLsnMap; + } + + /** + * Map of active voice talkgroups and DMR logical slots where they are active. + */ + public Map getActiveIdentifierMap() + { + if(mActiveIdentifierMap == null) { - hasIdentifier = true; - int highLsnBitmap = pointer; + mActiveIdentifierMap = new HashMap<>(); + mActiveLsnMap = new HashMap<>(); - pointer += 8; + int pointer = LSN_1_8_BITMAP_START; - for(int x = highLsnBitmap; x < (highLsnBitmap + 8); x++) + //Process voice LSNs 1-8 + if(getMessage().getInt(BYTE, pointer) > 0) { - int lsn = x - highLsnBitmap + 1 + 8; + int lowLsnBitmap = pointer; + pointer += 8; - if(getMessage().get(x)) + for(int x = lowLsnBitmap; x < (lowLsnBitmap + 8); x++) { - if(pointer <= 72) - { - talkgroups[lsn] = getMessage().getInt(BYTE, pointer); - pointer += 8; - } - else + int lsn = x - lowLsnBitmap + 1; + + if(getMessage().get(x)) { - talkgroups[lsn] = -1; + if(pointer <= 72) + { + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(getMessage().getInt(BYTE, pointer))); + pointer += 8; + } + else + { + //Create a placeholder for the talkgroup since we don't know the value + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(-1)); + } } } } - } - else - { - pointer += 8; - } - - //Process Radio IDs - first bit in radio options byte is set to indicate more activity - if(pointer <= 72 && getMessage().get(pointer)) - { - hasIdentifier = true; - pointer += 8; + else + { + pointer += 8; + } - //If we have the data revert channel bitmap ... + //Process voice LSNs 9 - 16 if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0) { - int lowDataLsnBitmap = pointer; + int highLsnBitmap = pointer; pointer += 8; - for(int x = lowDataLsnBitmap; x < (lowDataLsnBitmap + 8); x++) + for(int x = highLsnBitmap; x < (highLsnBitmap + 8); x++) { - int lsn = x - lowDataLsnBitmap + 1; + int lsn = x - highLsnBitmap + 1 + 8; if(getMessage().get(x)) { - if(pointer <= 64) + if(pointer <= 72) { - radios[lsn] = getMessage().getInt(TWO_BYTES, pointer); - pointer += 16; + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(getMessage().getInt(BYTE, pointer))); + pointer += 8; } else { - radios[lsn] = -1; + //Create a placeholder for the talkgroup since we don't know the value + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRTalkgroup.create(-1)); } } } } - } - - StringBuilder sb = new StringBuilder(); + else + { + pointer += 8; + } - if(!hasIdentifier) - { - sb.append("IDLE LSN: 1-16"); - } - else - { - sb.append("ACTIVE LSN"); - for(int x = 1; x < 17; x++) + //Process Radio IDs - first bit in radio options byte is set to indicate more activity + if(pointer <= 72 && getMessage().get(pointer)) { - if(talkgroups[x] != 0 || radios[x] != 0) + pointer += 8; + + //If we have the data revert channel bitmap ... + if(pointer <= 72 && getMessage().getInt(BYTE, pointer) > 0) { - sb.append(" ").append(x); + int lowDataLsnBitmap = pointer; + + pointer += 8; - if(talkgroups[x] == 0) + for(int x = lowDataLsnBitmap; x < (lowDataLsnBitmap + 8); x++) { - if(radios[x] < 0) - { - sb.append(":(R)A"); //Radio active on channel but identifier is in a continuation message - } - else if(radios[x] > 0) - { - sb.append(":(R)").append(radios[x]); //Active radio ID for channel - } - else + int lsn = x - lowDataLsnBitmap + 1; + + if(getMessage().get(x)) { - sb.append(":"); //No radio or talkgroup active on channel + if(pointer <= 64) + { + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRRadio.createTo(getMessage().getInt(TWO_BYTES, pointer))); + pointer += 16; + } + else + { + //Create a placeholder for the radio since we don't know the value + mActiveLsnMap.put(lsn, new DMRLsn(lsn)); + mActiveIdentifierMap.put(lsn, DMRRadio.createTo(-1)); + } } } - else if(talkgroups[x] < 0) - { - sb.append(":(T)A"); //Talkgroup active on channel but identifier is in a continuation message - } - else - { - sb.append(":(T)").append(talkgroups[x]); //Active talkgroup ID for channel. - } } } } - return sb.toString(); + return mActiveIdentifierMap; } - /** * Segment indicator for system status message values that are fragmented across multiple system status * messages. @@ -301,7 +343,15 @@ public int getRestLSN() @Override public int[] getLogicalChannelNumbers() { - return getRestChannel().getLogicalChannelNumbers(); + Set lcnSet = new HashSet<>(); + lcnSet.add(getRestChannel().getChannelNumber()); + + for(DMRLsn lsn: getActiveLsnMap().values()) + { + lcnSet.add(lsn.getChannelNumber()); + } + + return lcnSet.stream().mapToInt(v -> v).toArray(); } /** @@ -313,6 +363,10 @@ public int[] getLogicalChannelNumbers() public void apply(List timeslotFrequencies) { getRestChannel().apply(timeslotFrequencies); + for(DMRLsn lsn: getActiveLsnMap().values()) + { + lsn.apply(timeslotFrequencies); + } } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java index 53b2de633..badd68b06 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java @@ -1,9 +1,27 @@ +/* + * ***************************************************************************** + * 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.dmr.message.data.lc.full.motorola; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.alias.DmrTalkerAlias2Identifier; - +import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier; import java.util.ArrayList; import java.util.List; @@ -14,7 +32,7 @@ public class CapacityMaxTalkerAliasContinuation extends CapacityPlusVoiceChannel { private static final int ALIAS_START = 16; private static final int ALIAS_END = ALIAS_START + (7 * 8); - private DmrTalkerAlias2Identifier mTalkerAliasIdentifier; + private DmrTalkerAliasIdentifier mTalkerAliasIdentifier; private List mIdentifiers; /** @@ -56,12 +74,12 @@ public String toString() * Talker alias identifier * @return identifier */ - public DmrTalkerAlias2Identifier getTalkerAliasIdentifier() + public DmrTalkerAliasIdentifier getTalkerAliasIdentifier() { if(mTalkerAliasIdentifier == null) { String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim(); - mTalkerAliasIdentifier = DmrTalkerAlias2Identifier.create(alias); + mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias); } return mTalkerAliasIdentifier; diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 581258f20..952ea3e12 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.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 @@ -273,19 +273,7 @@ public void setFrequency(long frequency) throws SourceException */ public long getFrequency() { - long frequency; - - try - { - getFrequencyControllerLock().lock(); - frequency = mFrequencyController.getFrequency(); - } - finally - { - getFrequencyControllerLock().unlock(); - } - - return frequency; + return mFrequencyController.getFrequency(); } @Override