diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java index 15cf2e17..c7814192 100644 --- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java +++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java @@ -307,8 +307,8 @@ public class BinaryMessage extends BitSet /** * Overrides the parent method which returns a BitSet so that we can return a BinaryMessage - * @param from - * @param to + * @param from inclusive + * @param to exclusive * @return */ @Override @@ -1198,9 +1198,22 @@ public class BinaryMessage extends BitSet { byte[] bytes = new byte[characterCount]; + int index; for(int x = 0; x < characterCount; x++) { - bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset); + index = x * 7 + offset; + + if(index == 0) + { + bytes[x] = getByte(CHARACTER_8_BIT, index); + bytes[x] = (byte)((bytes[x] >> 1) & 0x7F); + } + else + { + index--; + bytes[x] = getByte(CHARACTER_8_BIT, index); + bytes[x] &= (byte)0x7F; //mask off the high bit since we have to parse it as an 8-bit byte. + } } return new String(bytes); diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java index f98f774a..c9e7a223 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java @@ -241,7 +241,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel } else { - String value = id.getValue().replace(" TS0", "").replace(" TS1", ""); + String value = id.getValue().replace(" TS1", "") + .replace(" TS2", ""); return value + " TS:" + channelMetadata.getTimeslot(); } } diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java index 811a8365..dfae0cfa 100644 --- a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java @@ -95,7 +95,7 @@ public abstract class DecoderState extends AbstractDecoderState /** * Sets the current channel descriptor */ - protected void setCurrentChannel(IChannelDescriptor channel) + public void setCurrentChannel(IChannelDescriptor channel) { mCurrentChannel = channel; } diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java index b6cfc28c..f6c5a9ef 100644 --- a/src/main/java/io/github/dsheirer/identifier/Form.java +++ b/src/main/java/io/github/dsheirer/identifier/Form.java @@ -54,6 +54,7 @@ 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 new file mode 100644 index 00000000..f689dd90 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAlias2Identifier.java @@ -0,0 +1,54 @@ +/* + * ***************************************************************************** + * 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 new file mode 100644 index 00000000..5d52285e --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAlias2Identifier.java @@ -0,0 +1,46 @@ +/* + * ***************************************************************************** + * 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 20a6729d..da388ee4 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -47,7 +47,10 @@ import io.github.dsheirer.module.decode.dmr.DMRDecoderState; import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager; 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.message.filter.DmrMessageFilterSet; +import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder; import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState; import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter; @@ -416,22 +419,50 @@ public class DecoderFactory DMRTrafficChannelManager dmrTrafficChannelManager = null; + if(trafficChannelManager instanceof DMRTrafficChannelManager) + { + dmrTrafficChannelManager = (DMRTrafficChannelManager)trafficChannelManager; + } + else + { + dmrTrafficChannelManager = new DMRTrafficChannelManager(channel); + } + + //Only register the traffic channel manager as a module if this is the parent control channel. if(channel.isStandardChannel()) { - if(trafficChannelManager instanceof DMRTrafficChannelManager) - { - dmrTrafficChannelManager = (DMRTrafficChannelManager)trafficChannelManager; - } - else - { - dmrTrafficChannelManager = new DMRTrafficChannelManager(channel); - } - modules.add(dmrTrafficChannelManager); } - modules.add(new DMRDecoderState(channel, 1, dmrTrafficChannelManager)); - modules.add(new DMRDecoderState(channel, 2, dmrTrafficChannelManager)); + DMRDecoderState state1 = new DMRDecoderState(channel, 1, dmrTrafficChannelManager); + DMRDecoderState state2 = new DMRDecoderState(channel, 2, dmrTrafficChannelManager); + + if(decodeConfig.hasChannelGrantEvent()) + { + DecodeEvent event = decodeConfig.getChannelGrantEvent(); + + if(decodeConfig.getChannelGrantEvent().getTimeslot() == 1) + { + state1.setCurrentCallEvent(event); + + if(event.getChannelDescriptor() instanceof DMRChannel dmrChannel) + { + state2.setCurrentChannel(dmrChannel.getSisterTimeslot()); + } + } + else + { + state2.setCurrentCallEvent(event); + + if(event.getChannelDescriptor() instanceof DMRChannel dmrChannel) + { + state1.setCurrentChannel(dmrChannel.getSisterTimeslot()); + } + } + } + + modules.add(state1); + modules.add(state2); modules.add(new DMRAudioModule(userPreferences, aliasList, 1)); modules.add(new DMRAudioModule(userPreferences, aliasList, 2)); 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 a73e7446..f83f5960 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 @@ -33,7 +33,7 @@ import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.integer.IntegerIdentifier; +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; @@ -44,6 +44,8 @@ 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.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; @@ -66,6 +68,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasComp import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; @@ -81,6 +85,7 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParam import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.DecodeEventType; +import io.github.dsheirer.module.decode.event.IDecodeEvent; import io.github.dsheirer.module.decode.event.PlottableDecodeEvent; import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket; @@ -92,13 +97,12 @@ import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket; import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest; import io.github.dsheirer.util.PacketUtil; -import java.util.HashMap; -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. @@ -106,16 +110,16 @@ import org.slf4j.LoggerFactory; public class DMRDecoderState extends TimeslotDecoderState { private final static Logger mLog = LoggerFactory.getLogger(DMRDecoderState.class); + private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog); private static final long MAX_VALID_CALL_DURATION_MS = 30000; + private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST = + new AddChannelRotationActiveStateRequest(State.ACTIVE); private Channel mChannel; private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor; private DMRTrafficChannelManager mTrafficChannelManager; private DecodeEvent mCurrentCallEvent; private long mCurrentFrequency; private boolean mIgnoreCRCChecksums; - private Map mDetectedCallEventsMap = new HashMap<>(); - private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST = - new AddChannelRotationActiveStateRequest(State.ACTIVE); /** * Constructs an DMR decoder state with an optional traffic channel manager. @@ -143,6 +147,17 @@ public class DMRDecoderState extends TimeslotDecoderState } } + @Override + protected void broadcast(IDecodeEvent event) + { + super.broadcast(event); + + if(mChannel.isTrafficChannel() && hasTrafficChannelManager()) + { + mTrafficChannelManager.receiveTrafficChannelEvent(event); + } + } + /** * Indicates if the message is valid or if the Ignore CRC Checksums feature is enabled. * @param message to check @@ -161,6 +176,16 @@ public class DMRDecoderState extends TimeslotDecoderState return mTrafficChannelManager != null; } + /** + * Sets the initial call event for a traffic channel. + * @param decodeEvent to use as the initial call event. + */ + public void setCurrentCallEvent(DecodeEvent decodeEvent) + { + mCurrentCallEvent = decodeEvent; + setCurrentChannel(decodeEvent.getChannelDescriptor()); + } + /** * Processes channel configuration change notifications received over the processing chain event bus. This is * primarily used for Capacity+ systems when the standard channel is converted to a traffic channel. In response, @@ -197,7 +222,6 @@ public class DMRDecoderState extends TimeslotDecoderState resetState(); mCurrentFrequency = 0; - mDetectedCallEventsMap.clear(); } /** @@ -307,6 +331,7 @@ public class DMRDecoderState extends TimeslotDecoderState broadcast(new DecoderStateEvent(this, Event.START, State.DATA, getTimeslot())); DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp()) + .channel(getCurrentChannel()) .details("MESSAGE: " + sms.getSMS()) .identifiers(new IdentifierCollection(sms.getIdentifiers())) .timeslot(getTimeslot()) @@ -331,6 +356,7 @@ public class DMRDecoderState extends TimeslotDecoderState mic.update(hyteraSmsPacket.getDestination()); DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details("SMS:" + hyteraSmsPacket.getSMS()) @@ -346,6 +372,7 @@ public class DMRDecoderState extends TimeslotDecoderState sb.append("HYTERA RRS REGISTER RADIO:"); sb.append(rrs.getDestination()); DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details(sb.toString()) @@ -371,6 +398,7 @@ public class DMRDecoderState extends TimeslotDecoderState sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString()); DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details(sb.toString()) @@ -383,6 +411,7 @@ public class DMRDecoderState extends TimeslotDecoderState MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString()) @@ -395,6 +424,7 @@ public class DMRDecoderState extends TimeslotDecoderState MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details(ars.toString()) @@ -407,6 +437,7 @@ public class DMRDecoderState extends TimeslotDecoderState MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details(lrrp.toString()) @@ -419,6 +450,7 @@ public class DMRDecoderState extends TimeslotDecoderState MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(mic) .timeslot(getTimeslot()) .details(xcmp.toString()) @@ -428,6 +460,7 @@ public class DMRDecoderState extends TimeslotDecoderState else { DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(new IdentifierCollection(packet.getIdentifiers())) .timeslot(getTimeslot()) .details(packet.toString()) @@ -760,15 +793,17 @@ public class DMRDecoderState extends TimeslotDecoderState case MOTOROLA_CONPLUS_NEIGHBOR_REPORT: broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; + case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT: + case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT: case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM: + case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM: case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM: - if(csbk instanceof ChannelGrant) + case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: + if(csbk instanceof ChannelGrant channelGrant) { - ChannelGrant dataGrant = (ChannelGrant)csbk; - DMRChannel channel = dataGrant.getChannel(); - + DMRChannel channel = channelGrant.getChannel(); if(hasTrafficChannelManager()) { @@ -778,90 +813,13 @@ public class DMRDecoderState extends TimeslotDecoderState } else { - DecodeEvent event = mDetectedCallEventsMap.get(channel); - - if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) - { - event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, - new IdentifierCollection(csbk.getIdentifiers())); - mDetectedCallEventsMap.put(channel, event); - } - else - { - //Update the ending timestamp for the event and rebroadcast - event.end(csbk.getTimestamp()); - } - - broadcast(event); + LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2, + "No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name()); } } - break; - case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: - case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: - if(csbk instanceof ChannelGrant) + else { - ChannelGrant tgGrant = (ChannelGrant)csbk; - DMRChannel channel = tgGrant.getChannel(); - - if(hasTrafficChannelManager()) - { - IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); - - mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, - csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); - } - else - { - DecodeEvent event = mDetectedCallEventsMap.get(channel); - - if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) - { - event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, - new IdentifierCollection(csbk.getIdentifiers())); - mDetectedCallEventsMap.put(channel, 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: - case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT: - if(csbk instanceof ChannelGrant) - { - ChannelGrant channelGrant = (ChannelGrant)csbk; - DMRChannel channel = channelGrant.getChannel(); - - if(hasTrafficChannelManager()) - { - IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); - - mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, - csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); - } - else - { - DecodeEvent event = mDetectedCallEventsMap.get(channel); - - if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) - { - event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel, - new IdentifierCollection(csbk.getIdentifiers())); - mDetectedCallEventsMap.put(channel, event); - } - else - { - //Update the ending timestamp for the event and rebroadcast - event.end(csbk.getTimestamp()); - } - - broadcast(event); - } + mLog.error("Unrecognized DMR channel grant CSBK ignored: " + csbk.getClass()); } break; case MOTOROLA_CAPMAX_ALOHA: @@ -877,42 +835,113 @@ public class DMRDecoderState extends TimeslotDecoderState } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; - case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT: - if(csbk instanceof ConnectPlusDataChannelGrant) + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE: + if(csbk instanceof CapacityMaxOpenModeVoiceChannelUpdate update) + { + if(hasTrafficChannelManager()) + { + if(update.hasTimeslot1()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannelTS1()); + mic.update(update.getTalkgroupTS1()); + mTrafficChannelManager.processChannelGrant(update.getChannelTS1(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + if(update.hasTimeslot2()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannelTS2()); + mic.update(update.getTalkgroupTS2()); + mTrafficChannelManager.processChannelGrant(update.getChannelTS2(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + } + else + { + LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2, + "No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name()); + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE: + if(csbk instanceof CapacityMaxAdvantageModeVoiceChannelUpdate update) + { + if(hasTrafficChannelManager()) + { + if(update.hasChannel1Timeslot1()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannel1TS1()); + mic.update(update.getTalkgroupCH1TS1()); + mTrafficChannelManager.processChannelGrant(update.getChannel1TS1(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + + if(update.hasChannel1Timeslot2()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannel1TS2()); + mic.update(update.getTalkgroupCH1TS2()); + mTrafficChannelManager.processChannelGrant(update.getChannel1TS2(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + + if(update.hasChannel2Timeslot1()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannel2TS1()); + mic.update(update.getTalkgroupCH2TS1()); + mTrafficChannelManager.processChannelGrant(update.getChannel2TS1(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + + if(update.hasChannel2Timeslot2()) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(update.getChannel2TS2()); + mic.update(update.getTalkgroupCH2TS2()); + mTrafficChannelManager.processChannelGrant(update.getChannel2TS2(), mic, csbk.getOpcode(), + csbk.getTimestamp(), csbk.isEncrypted()); + } + } + else + { + LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2, + "No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name()); + } + } + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); + break; + case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT: + if(csbk instanceof ConnectPlusDataChannelGrant cpdcg) { - ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk; DMRChannel channel = cpdcg.getChannel(); if(hasTrafficChannelManager()) { IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); - mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); } else { - DecodeEvent event = mDetectedCallEventsMap.get(channel); - - if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) - { - event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, - new IdentifierCollection(csbk.getIdentifiers())); - mDetectedCallEventsMap.put(channel, event); - } - else - { - //Update the ending timestamp for the event and rebroadcast - event.end(csbk.getTimestamp()); - } - - broadcast(event); + LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2, + "No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name()); } } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; case MOTOROLA_CONPLUS_REGISTRATION_REQUEST: DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp()) + .channel(getCurrentChannel()) .details("Registration Request") .identifiers(new IdentifierCollection(csbk.getIdentifiers())) .timeslot(getTimeslot()) @@ -922,6 +951,7 @@ public class DMRDecoderState extends TimeslotDecoderState break; case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE: DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp()) + .channel(getCurrentChannel()) .details("Registration Response") .identifiers(new IdentifierCollection(csbk.getIdentifiers())) .timeslot(getTimeslot()) @@ -930,41 +960,27 @@ public class DMRDecoderState extends TimeslotDecoderState broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER: - if(csbk instanceof ConnectPlusVoiceChannelUser) + if(csbk instanceof ConnectPlusVoiceChannelUser cpvcu) { - ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk; DMRChannel channel = cpvcu.getChannel(); if(hasTrafficChannelManager()) { IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers()); - mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); } else { - DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel); - - if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers())) - { - detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, - new IdentifierCollection(csbk.getIdentifiers())); - mDetectedCallEventsMap.put(channel, detectedEvent); - } - else - { - //Update the ending timestamp for the event and rebroadcast - detectedEvent.end(csbk.getTimestamp()); - } - - broadcast(detectedEvent); + LOGGING_SUPPRESSOR.error("NoTCM" + csbk.getOpcode().name(), 2, + "No DMR Traffic Channel Manager available for channel grant-" + csbk.getOpcode().name()); } } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); break; case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION: DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp()) + .channel(getCurrentChannel()) .details("TALKGROUP AFFILIATION") .identifiers(new IdentifierCollection(csbk.getIdentifiers())) .timeslot(getTimeslot()) @@ -978,18 +994,9 @@ public class DMRDecoderState extends TimeslotDecoderState } } - private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel, - IdentifierCollection identifierCollection) { - return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp()) - .channel(channel) - .details(csbk.getOpcode().getLabel()) - .identifiers(identifierCollection) - .timeslot(getTimeslot()) - .build(); - } - private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) { return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp()) + .channel(getCurrentChannel()) .identifiers(new IdentifierCollection(csbk.getIdentifiers())) .timeslot(getTimeslot()) .details(details) @@ -1010,82 +1017,6 @@ public class DMRDecoderState extends TimeslotDecoderState return mic; } - /** - * 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, List currentIdentifiers) - { - if(event == null || (timestamp - event.getTimeStart() > MAX_VALID_CALL_DURATION_MS)) - { - return true; - } - - return !isSameCall(event.getIdentifierCollection(), currentIdentifiers); - } - - /** - * Indicates if the TO/FROM identifiers in the identifier collection match the TO/FROM identifiers in the list - * of identifiers. - * @param identifierCollection containing TO/FROM identifiers - * @param identifiers containing TO/FROM identifiers - * @return true if the TO/FROM identifiers in each collection match - */ - private boolean isSameCall(IdentifierCollection identifierCollection, List identifiers) - { - IntegerIdentifier to = null; - IntegerIdentifier from = null; - - for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.TO)) - { - if(identifier instanceof IntegerIdentifier) - { - to = (IntegerIdentifier)identifier; - break; - } - } - - for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.FROM)) - { - if(identifier instanceof IntegerIdentifier) - { - from = (IntegerIdentifier)identifier; - break; - } - } - - if(to == null || from == null) - { - return false; - } - - boolean toMatch = false; - boolean fromMatch = false; - - for(Identifier identifier: identifiers) - { - if(identifier.getRole() == Role.TO && - identifier instanceof IntegerIdentifier && - ((IntegerIdentifier)identifier).getValue() == to.getValue()) - { - toMatch = true; - } - else if(identifier.getRole() == Role.FROM && - identifier instanceof IntegerIdentifier && - ((IntegerIdentifier)identifier).getValue() == from.getValue()) - { - fromMatch = true; - } - } - - return toMatch & fromMatch; - } - /** * Processes Link Control Messages * @param isTerminator set to true when the link control is carried by a terminator @@ -1100,6 +1031,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(mCurrentCallEvent != null) { mCurrentCallEvent.setDetails(ep.getDetails()); + broadcast(mCurrentCallEvent); } } break; @@ -1114,6 +1046,7 @@ public class DMRDecoderState extends TimeslotDecoderState { if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(cpgvcu.getTalkgroup()); } @@ -1131,6 +1064,7 @@ public class DMRDecoderState extends TimeslotDecoderState { if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(cpgvcu.getTalkgroup()); } @@ -1148,6 +1082,7 @@ public class DMRDecoderState extends TimeslotDecoderState { if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(cmvcu.getTalkgroup()); } @@ -1160,6 +1095,28 @@ public class DMRDecoderState extends TimeslotDecoderState } } break; + case FULL_CAPACITY_MAX_TALKER_ALIAS: + if(message instanceof CapacityMaxTalkerAlias alias) + { + getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + + if(mCurrentCallEvent != null) + { + broadcast(mCurrentCallEvent); + } + } + break; + case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: + if(message instanceof CapacityMaxTalkerAliasContinuation alias) + { + getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); + + if(mCurrentCallEvent != null) + { + broadcast(mCurrentCallEvent); + } + } + break; case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER: if(message instanceof CapacityPlusWideAreaVoiceChannelUser) { @@ -1169,6 +1126,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(cpuo4.getTalkgroup()); } @@ -1185,6 +1143,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(hgvcu.getTalkgroup()); } @@ -1209,6 +1168,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(huuvcu.getTargetRadio()); } @@ -1228,6 +1188,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(gvcu.getTalkgroup()); } @@ -1247,6 +1208,7 @@ public class DMRDecoderState extends TimeslotDecoderState if(isTerminator) { + closeCurrentCallEvent(message.getTimestamp()); getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().update(uuvcu.getTargetRadio()); } @@ -1336,8 +1298,6 @@ public class DMRDecoderState extends TimeslotDecoderState .identifiers(getIdentifierCollection().copyOf()) .timeslot(getTimeslot()) .build(); - - broadcast(mCurrentCallEvent); } else { @@ -1348,9 +1308,10 @@ public class DMRDecoderState extends TimeslotDecoderState mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf()); mCurrentCallEvent.end(timestamp); - broadcast(mCurrentCallEvent); } + broadcast(mCurrentCallEvent); + if(type == DecodeEventType.CALL_GROUP_ENCRYPTED || type == DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED) { broadcast(new DecoderStateEvent(this, event, State.ENCRYPTED, getTimeslot())); @@ -1403,7 +1364,7 @@ public class DMRDecoderState extends TimeslotDecoderState mCurrentFrequency = event.getFrequency(); if(hasTrafficChannelManager()) { - mTrafficChannelManager.setCurrentControlFrequency(mCurrentFrequency); + mTrafficChannelManager.setCurrentControlFrequency(mCurrentFrequency, mChannel); } break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java index 8ce21167..8eb14c2e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java @@ -59,6 +59,8 @@ public class DMRNetworkConfigurationMonitor private static final String BRAND_TIER_3_TRUNKING = "Tier III Trunking"; private static final String BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING = "Capacity Max Tier III Trunking"; private static final String BRAND_HYTERA_TIER_3_TRUNKING = "Hytera Tier III Trunking"; + private static final String MODE_CAPACITY_MAX_OPEN_SYSTEM = "Open System"; + private static final String MODE_CAPACITY_MAX_ADVANTAGE = "Advantage"; private List mNeighborSites = new ArrayList<>(); private Map mTier3NeighborSites = new HashMap<>(); @@ -67,6 +69,7 @@ public class DMRNetworkConfigurationMonitor private DMRSite mDMRSite; private Model mTier3Model; private String mBrand; + private String mMode; private Integer mColorCode; private DMRChannel mCurrentChannel; private Channel mChannel; @@ -94,13 +97,13 @@ public class DMRNetworkConfigurationMonitor */ public void process(DMRMessage message) { - if(message instanceof CSBKMessage) + if(message instanceof CSBKMessage csbk) { - process((CSBKMessage)message); + process(csbk); } - else if(message instanceof LCMessage) + else if(message instanceof LCMessage lc) { - process((LCMessage)message); + process(lc); } } @@ -172,6 +175,12 @@ public class DMRNetworkConfigurationMonitor } } break; + case STANDARD_ANNOUNCEMENT: + if(csbk instanceof AdjacentSiteInformation neighbor) + { + mTier3NeighborSites.put(neighbor.getNeighborSystemIdentityCode().getSite().getValue(), neighbor); + } + break; case HYTERA_08_ANNOUNCEMENT: case HYTERA_68_ANNOUNCEMENT: if(csbk instanceof HyteraAnnouncement) @@ -240,6 +249,12 @@ public class DMRNetworkConfigurationMonitor } } break; + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE: + mMode = MODE_CAPACITY_MAX_ADVANTAGE; + break; + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE: + mMode = MODE_CAPACITY_MAX_OPEN_SYSTEM; + break; case MOTOROLA_CONPLUS_NEIGHBOR_REPORT: if(mNeighborSites.isEmpty() && csbk instanceof ConnectPlusNeighborReport) { @@ -297,6 +312,11 @@ public class DMRNetworkConfigurationMonitor //DMR System Brand Name sb.append("\n\nBrand:").append((mBrand == null ? BRAND_STANDARD : mBrand)); + if(mMode != null) + { + sb.append("\nConfigured Mode:").append(mMode); + } + if(mDMRNetwork != null) { sb.append("\nNetwork:").append(mDMRNetwork); @@ -347,6 +367,11 @@ public class DMRNetworkConfigurationMonitor { sb.append("\tNeighbor: ").append(neighbor).append("\n"); } + } + + if(!mTier3NeighborSites.isEmpty()) + { + sb.append("\nTier III Neighbor Sites\n"); for(Map.Entry neighbor: mTier3NeighborSites.entrySet()) { @@ -354,7 +379,16 @@ public class DMRNetworkConfigurationMonitor sb.append("\tNeighbor: ").append("Network:").append(site.getNeighborSystemIdentityCode().getNetwork()); sb.append(" Site:").append(site.getNeighborSystemIdentityCode().getSite()); sb.append(" ").append(site.getNeighborSystemIdentityCode().getModel()); - sb.append(" ").append(site.getNeighborChannel()).append("\n"); + + DMRChannel channel = site.getNeighborChannel(); + sb.append(" ").append(channel); + + if(channel.getDownlinkFrequency() > 0) + { + sb.append(" ").append(channel.getDownlinkFrequency()); + } + + sb.append("\n"); } } 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 9d0e18dd..8467ab01 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 @@ -37,7 +37,7 @@ import io.github.dsheirer.message.MessageHistoryRequest; 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.event.DMRDecodeEvent; 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; @@ -55,6 +55,7 @@ import io.github.dsheirer.source.config.SourceConfigTuner; 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; @@ -63,6 +64,8 @@ import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantLock; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,36 +86,32 @@ import org.slf4j.LoggerFactory; * * This manager monitors channel events watching for events related to managed traffic channels. */ -public class DMRTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener, - IChannelEventProvider +public class DMRTrafficChannelManager extends TrafficChannelManager implements IChannelEventListener, + IChannelEventProvider, IDecodeEventProvider { private final static Logger mLog = LoggerFactory.getLogger(DMRTrafficChannelManager.class); 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 String NO_FREQUENCY = "NO FREQUENCY - CHECK PLAYLIST CHANNEL CONFIG LSN CHANNEL MAP"; public static final long EVENT_TIME_STALE_THRESHOLD = 5000; //5 seconds - private Queue mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>(); - private List mManagedTrafficChannels; - - private Map mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>(); - private Map mChannelGrantEventMap = new ConcurrentHashMap<>(); - + private Queue mAvailableTrafficChannels = new ConcurrentLinkedQueue<>(); + private List mAllocatedTrafficChannels; + private Map mAllocatedChannelFrequencyMap = new ConcurrentHashMap<>(); + private ReentrantLock mLock = new ReentrantLock(); + private Map mCallEventsTS1 = new ConcurrentHashMap<>(); + private Map mCallEventsTS2 = new ConcurrentHashMap<>(); private Listener mChannelEventListener; private Listener mDecodeEventListener; - private TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor(); private Channel mParentChannel; + private boolean mIgnoreDataCalls; //Used as temporary storage for message and decode event history during Cap+ REST channel rotation private DecodeEventHistory mTransientDecodeEventHistory; private List mTransientMessageHistory; - private boolean mIgnoreDataCalls; - private long mCurrentControlFrequency; - /** * Monitors call events and allocates traffic decoder channels in response * to traffic channel allocation call events. Manages a pool of reusable @@ -136,10 +135,11 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I * Sets the current parent control channel frequency so that channel grants for the current frequency do not * produce an additional traffic channel allocation. * @param currentControlFrequency for current control channel. + * @param channel for the current control channel */ - public void setCurrentControlFrequency(long currentControlFrequency) + public void setCurrentControlFrequency(long currentControlFrequency, Channel channel) { - mCurrentControlFrequency = currentControlFrequency; + mAllocatedChannelFrequencyMap.put(currentControlFrequency, channel); } /** @@ -149,7 +149,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I */ private void createTrafficChannels() { - if(mManagedTrafficChannels == null) + if(mAllocatedTrafficChannels == null) { DecodeConfiguration decodeConfiguration = mParentChannel.getDecodeConfiguration(); List trafficChannelList = new ArrayList<>(); @@ -176,8 +176,8 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I } } - mAvailableTrafficChannelQueue.addAll(trafficChannelList); - mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList); + mAvailableTrafficChannels.addAll(trafficChannelList); + mAllocatedTrafficChannels = Collections.unmodifiableList(trafficChannelList); } } @@ -191,85 +191,92 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I public void convertToTrafficChannel(Channel channel, long currentFrequency, IChannelDescriptor restChannel, DMRNetworkConfigurationMonitor networkConfigurationMonitor) { - //Only do the conversion of the original channel has multiple frequencies defined and the rest channel is - //one of those frequencies - if(restChannel.getDownlinkFrequency() > 0 && - !mAllocatedTrafficChannelFrequencyMap.containsKey(restChannel.getDownlinkFrequency()) && - channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES) + mLock.lock(); + + try { - SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration(); + long rest = restChannel.getDownlinkFrequency(); - //Add the rest channel to the list of frequencies in the source configuration - if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency())) + //Only do the conversion of the original channel has multiple frequencies defined and the rest channel is + //one of those frequencies + if(rest > 0 && !mAllocatedChannelFrequencyMap.containsKey(rest) && + channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES) { - originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency()); + SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration(); - //Note: the channel configuration modification is not persisted from here, to avoid unnecessary coupling - //to the channel model. The configuration will simply be adjusted at runtime. - } - - //Convert current channel to a traffic channel if one is available - Channel trafficChannel = mAvailableTrafficChannelQueue.poll(); - - if(trafficChannel != null && restChannel.getDownlinkFrequency() > 0) - { - //Disable the channel rotation manager when we have multiple frequencies defined - getInterModuleEventBus().post(new DisableChannelRotationMonitorRequest()); - - //Set the frequency for the traffic channel configuration - SourceConfigTuner trafficSourceConfig = new SourceConfigTuner(); - trafficSourceConfig.setFrequency(currentFrequency); - trafficChannel.setSourceConfiguration(trafficSourceConfig); - - //Post a request for message and decode event history to transfer to the new REST channel. This has - // to be posted before the channel conversion request - getInterModuleEventBus().post(new DecodeEventHistoryRequest()); - getInterModuleEventBus().post(new MessageHistoryRequest()); - - //Dispatch a request to convert this processing chain to the traffic channel. This will cause the - //processing chain to convert to a traffic channel and notify all of the modules, which will in-turn - //cause the decoder states (2 timeslots) to dereference this manager so that the existing channel can - //no longer allocate traffic channels. - getInterModuleEventBus().post(new ChannelConversionRequest(channel, trafficChannel)); - - mAllocatedTrafficChannelFrequencyMap.put(currentFrequency, trafficChannel); - - //Set the preferred frequency to use when restarting the original channel - originalSourceConfig.setPreferredFrequency(restChannel.getDownlinkFrequency()); - - //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); - request.setPersistentAttempt(true); - request.setChildDecodeEventHistory(mTransientDecodeEventHistory); - - //If we received an event history response, add it to the request as preload data content - if(mTransientDecodeEventHistory != null) - { - DecodeEventHistoryPreloadData eventHistory = - new DecodeEventHistoryPreloadData(mTransientDecodeEventHistory.getItems()); - request.addPreloadDataContent(eventHistory); - - mTransientDecodeEventHistory = null; - } - - //If we received a message history response, add it to the request as preload data content - if(mTransientMessageHistory != null) - { - MessageHistoryPreloadData messageHistory = new MessageHistoryPreloadData(mTransientMessageHistory); - request.addPreloadDataContent(messageHistory); - mTransientMessageHistory = null; - } - - //Add the DMR network configuration monitor as preload data - if(networkConfigurationMonitor != null) - { - request.addPreloadDataContent(new DMRNetworkConfigurationPreloadData(networkConfigurationMonitor)); - } - - getInterModuleEventBus().post(request); + //Add the rest channel to the list of frequencies in the source configuration + if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency())) + { + originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency()); + } + + //Convert current channel to a traffic channel if one is available + Channel trafficChannel = mAvailableTrafficChannels.poll(); + + if(trafficChannel != null) + { + //Disable the channel rotation manager when we have multiple frequencies defined + getInterModuleEventBus().post(new DisableChannelRotationMonitorRequest()); + + //Set the frequency for the traffic channel configuration + SourceConfigTuner trafficSourceConfig = new SourceConfigTuner(); + trafficSourceConfig.setFrequency(currentFrequency); + trafficChannel.setSourceConfiguration(trafficSourceConfig); + + //Post a request for message and decode event history to transfer to the new REST channel. This has + // to be posted before the channel conversion request + getInterModuleEventBus().post(new DecodeEventHistoryRequest()); + getInterModuleEventBus().post(new MessageHistoryRequest()); + + //Dispatch a request to convert this processing chain to the traffic channel. This will cause the + //processing chain to convert to a traffic channel and notify all of the modules, which will in-turn + //cause the decoder states (2 timeslots) to dereference this manager so that the existing channel can + //no longer allocate traffic channels. + getInterModuleEventBus().post(new ChannelConversionRequest(channel, trafficChannel)); + + mAllocatedChannelFrequencyMap.put(currentFrequency, trafficChannel); + + //Set the preferred frequency to use when restarting the original channel + originalSourceConfig.setPreferredFrequency(rest); + + //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); + request.setPersistentAttempt(true); + request.setChildDecodeEventHistory(mTransientDecodeEventHistory); + + //If we received an event history response, add it to the request as preload data content + if(mTransientDecodeEventHistory != null) + { + DecodeEventHistoryPreloadData eventHistory = + new DecodeEventHistoryPreloadData(mTransientDecodeEventHistory.getItems()); + request.addPreloadDataContent(eventHistory); + + mTransientDecodeEventHistory = null; + } + + //If we received a message history response, add it to the request as preload data content + if(mTransientMessageHistory != null) + { + MessageHistoryPreloadData messageHistory = new MessageHistoryPreloadData(mTransientMessageHistory); + request.addPreloadDataContent(messageHistory); + mTransientMessageHistory = null; + } + + //Add the DMR network configuration monitor as preload data + if(networkConfigurationMonitor != null) + { + request.addPreloadDataContent(new DMRNetworkConfigurationPreloadData(networkConfigurationMonitor)); + } + + getInterModuleEventBus().post(request); + } } } + finally + { + mLock.unlock(); + } } /** @@ -301,7 +308,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I /** * Broadcasts an initial or update decode event to any registered listener. */ - public void broadcast(DecodeEvent decodeEvent) + public void broadcast(IDecodeEvent decodeEvent) { if(mDecodeEventListener != null) { @@ -309,119 +316,159 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I } } - public void processEndChannelGrant() + /** + * Receive decode events/updates from traffic channels to capture in the parent channel's decode event history. We + * do this by rebroadcasting the event to the parent control channel's processing chain so that the event history + * can receive the event or update. + * @param trafficChannelEvent to receive and rebroadcast. + */ + public void receiveTrafficChannelEvent(IDecodeEvent trafficChannelEvent) { - //TODO: process the CSBK opcode that indicates traffic channel teardown + broadcast(trafficChannelEvent); } /** * Processes channel grants to allocate traffic channels and track overall channel usage. Generates * decode events for each new channel that is allocated. - * */ public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection, Opcode opcode, long timestamp, boolean encrypted) { - DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel); - DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted); + mLock.lock(); - if(isStale(event, timestamp, identifierCollection)) //Create new event + try { - event = DMRChannelGrantEvent.channelGrantBuilder(decodeEventType, timestamp) - .channel(channel) - .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) - .identifiers(identifierCollection) - .build(); + boolean allocated = mAllocatedChannelFrequencyMap.containsKey(channel.getDownlinkFrequency()); - mChannelGrantEventMap.put(channel, event); - - } - else //Update current event - { - Identifier from = getIdentifier(identifierCollection, Role.FROM); - - if(from != null) + if(allocated) { - Identifier currentFrom = getIdentifier(event.getIdentifierCollection(), Role.FROM); - if(currentFrom != null && !Objects.equals(from, currentFrom)) - { - event.end(timestamp); - - event = DMRChannelGrantEvent.channelGrantBuilder(decodeEventType, timestamp) - .channel(channel) - .details("CONTINUE - CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) - .identifiers(identifierCollection) - .build(); - - mChannelGrantEventMap.put(channel, 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 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) - { - 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.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) - { - SourceConfigTuner sourceConfig = new SourceConfigTuner(); - sourceConfig.setFrequency(frequency); - trafficChannel.setSourceConfiguration(sourceConfig); - mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel); - getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, - identifierCollection)); + //Note: if traffic channel is allocated then do nothing & let the channel maintain its own event state } else { - if(event.getDetails() == null) + long frequency = channel.getDownlinkFrequency(); + + //If we don't have a frequency value for the channel, use the channel number as a place holder. We won't + //allocate a traffic channel for it, but we can track the related channel grant event. + if(frequency == 0) { - event.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED); + frequency = channel.getChannelNumber(); } - else if(!event.getDetails().endsWith(MAX_TRAFFIC_CHANNELS_EXCEEDED)) + + Map eventMap = channel.getTimeslot() == 1 ? mCallEventsTS1 : mCallEventsTS2; + IDecodeEvent temp = eventMap.get(frequency); + DecodeEvent event = (temp != null) ? (DecodeEvent)temp : null; + + if(isStale(event, timestamp, identifierCollection)) //Create new event { - event.setDetails(event.getDetails() + " - " + MAX_TRAFFIC_CHANNELS_EXCEEDED); + DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted); + event = DMRDecodeEvent.builder(decodeEventType, timestamp) + .channel(channel) + .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) + .identifiers(identifierCollection) + .timeslot(channel.getTimeslot()) + .build(); + eventMap.put(frequency, event); } + else if(event != null) //Update current event + { + Identifier from = getIdentifier(identifierCollection, Role.FROM); + + if(from != null) + { + Identifier currentFrom = getIdentifier(event.getIdentifierCollection(), Role.FROM); + if(currentFrom != null && !Objects.equals(from, currentFrom)) + { + event.end(timestamp); + + DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted); + event = DMRDecodeEvent.builder(decodeEventType, timestamp) + .channel(channel) + .details("CONTINUE - CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : "")) + .identifiers(identifierCollection) + .timeslot(channel.getTimeslot()) + .build(); + + eventMap.put(frequency, event); + } + } + + //update the ending timestamp so that the duration value is correctly calculated + event.update(timestamp); + } + + //If our channel doesn't have a frequency value, update the event to reflect this. + if(channel.getDownlinkFrequency() == 0) + { + if(event.getDetails() == null) + { + event.setDetails(NO_FREQUENCY); + } + else if(!event.getDetails().endsWith(NO_FREQUENCY)) + { + event.setDetails(event.getDetails() + " - " + NO_FREQUENCY); + } + + broadcast(event); + return; + } + + if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode()) + { + 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 = mAvailableTrafficChannels.poll(); + + if(trafficChannel != null) + { + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency(frequency); + trafficChannel.setSourceConfiguration(sourceConfig); + mAllocatedChannelFrequencyMap.put(frequency, trafficChannel); + //Preload the channel grant event for the traffic channel to use/maintain + if(trafficChannel.getDecodeConfiguration() instanceof DecodeConfigDMR config) + { + config.setChannelGrantEvent(event); + } + + getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, + identifierCollection, this)); + } + 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(event); } } + catch(Exception e) + { + //This shouldn't happen, but we'll log if it ever does. + mLog.error("Error while processing channel grant event - traffic channel lock is released", e); + } + finally + { + mLock.unlock(); + } } @@ -436,6 +483,8 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I { case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT: case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT: + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE: + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE: type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP; break; @@ -540,7 +589,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I * @param currentIdentifiers to compare against the event * @return true if the event is stale. */ - private boolean isStale(DecodeEvent event, long timestamp, IdentifierCollection currentIdentifiers) + private boolean isStale(IDecodeEvent event, long timestamp, IdentifierCollection currentIdentifiers) { if(event == null) { @@ -609,23 +658,41 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I @Override public void start() { - for(Long frequency: mAllocatedTrafficChannelFrequencyMap.keySet()) + mLock.lock(); + + try { - getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(frequency)); + for(Long frequency: mAllocatedChannelFrequencyMap.keySet()) + { + getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(frequency)); + } + } + finally + { + mLock.unlock(); } } @Override public void stop() { - mAvailableTrafficChannelQueue.clear(); + mLock.lock(); - List channels = new ArrayList<>(mAllocatedTrafficChannelFrequencyMap.values()); - - //Issue a disable request for each traffic channel - for(Channel channel: channels) + try { - broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); + mAvailableTrafficChannels.clear(); + + List channels = new ArrayList<>(mAllocatedChannelFrequencyMap.values()); + + //Issue a disable request for each traffic channel + for(Channel channel: channels) + { + broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); + } + } + finally + { + mLock.unlock(); } } @@ -641,54 +708,71 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I */ private void removeCallEvents(long frequency) { - List channelsToRemove = new ArrayList<>(); + mLock.lock(); - for(Map.Entry entry: mChannelGrantEventMap.entrySet()) + try { - if(entry.getKey().getDownlinkFrequency() == frequency) - { - channelsToRemove.add(entry.getKey()); - } + mCallEventsTS1.remove(frequency); + mCallEventsTS2.remove(frequency); } - - for(DMRChannel channel: channelsToRemove) + finally { - mChannelGrantEventMap.remove(channel); + mLock.unlock(); } } - private void updateCallEventDetails(long frequency, String detailFragment) + /** + * Updates any call events related to the frequency to indicate the channel start was rejected. + * @param frequency for the channel. + */ + private void setChannelStartRejected(long frequency) { - List channelsToUpdate = new ArrayList<>(); + mLock.lock(); - for(Map.Entry entry: mChannelGrantEventMap.entrySet()) + try { - if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency) + IDecodeEvent event = mCallEventsTS1.get(frequency); + + if(event instanceof DecodeEvent decodeEvent1) { - channelsToUpdate.add(entry.getKey()); + if(decodeEvent1.getDetails() == null) + { + decodeEvent1.setDetails(CHANNEL_START_REJECTED); + } + else if(!decodeEvent1.getDetails().endsWith(CHANNEL_START_REJECTED)) + { + decodeEvent1.setDetails(event.getDetails() + "-" + CHANNEL_START_REJECTED); + } + + broadcast(decodeEvent1); + } + + event = mCallEventsTS2.get(frequency); + + if(event instanceof DecodeEvent decodeEvent2) + { + if(decodeEvent2.getDetails() == null) + { + decodeEvent2.setDetails(CHANNEL_START_REJECTED); + } + else if(!decodeEvent2.getDetails().endsWith(CHANNEL_START_REJECTED)) + { + decodeEvent2.setDetails(decodeEvent2.getDetails() + "-" + CHANNEL_START_REJECTED); + } + + broadcast(decodeEvent2); } } - - for(DMRChannel channel: channelsToUpdate) + finally { - DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel); - - if(event != null) - { - if(event.getDetails() == null) - { - event.setDetails(detailFragment); - } - else if(!event.getDetails().endsWith(detailFragment)) - { - event.setDetails(event.getDetails() + detailFragment); - } - - broadcast(event); - } + mLock.unlock(); } } + /** + * Process traffic channel processing stop or processing start rejected events. + * @param channelEvent to process + */ @Override public synchronized void receive(ChannelEvent channelEvent) { @@ -701,55 +785,72 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I case NOTIFICATION_PROCESSING_STOP: long frequencyToRemove = 0; - for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + mLock.lock(); + + try { - if(channel == entry.getValue() && entry.getKey() != null) + for(Map.Entry entry: mAllocatedChannelFrequencyMap.entrySet()) { - frequencyToRemove = entry.getKey(); - removeCallEvents(entry.getKey()); - break; + if(channel == entry.getValue() && entry.getKey() != null) + { + frequencyToRemove = entry.getKey(); + removeCallEvents(entry.getKey()); + break; + } + } + + if(frequencyToRemove > 0) + { + mAllocatedChannelFrequencyMap.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(!mAvailableTrafficChannels.contains(channel)) + { + mAvailableTrafficChannels.add(channel); } } - - if(frequencyToRemove > 0) + finally { - 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); + mLock.unlock(); } break; case NOTIFICATION_PROCESSING_START_REJECTED: - long frequencyToUpdate = 0; - - for(Map.Entry entry: mAllocatedTrafficChannelFrequencyMap.entrySet()) + mLock.lock(); + try { - if(channel == entry.getValue() && entry.getKey() != null) + long frequencyToUpdate = 0; + + for(Map.Entry entry: mAllocatedChannelFrequencyMap.entrySet()) { - frequencyToUpdate = entry.getKey(); - updateCallEventDetails(entry.getKey(), CHANNEL_START_REJECTED); - break; + if(channel == entry.getValue() && entry.getKey() != null) + { + frequencyToUpdate = entry.getKey(); + setChannelStartRejected(entry.getKey()); + break; + } + } + + if(frequencyToUpdate > 0) + { + mAllocatedChannelFrequencyMap.remove(frequencyToUpdate); + + //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(!mAvailableTrafficChannels.contains(channel)) + { + mAvailableTrafficChannels.add(channel); } } - - if(frequencyToUpdate > 0) + finally { - mAllocatedTrafficChannelFrequencyMap.remove(frequencyToUpdate); - - //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); + mLock.unlock(); } 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 23aa12c5..22a16168 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 @@ -23,6 +23,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.config.DecodeConfiguration; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; +import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.source.tuner.channel.ChannelSpecification; import java.util.ArrayList; import java.util.List; @@ -41,6 +42,9 @@ public class DecodeConfigDMR extends DecodeConfiguration private boolean mUseCompressedTalkgroups = false; private List mTimeslotMap = new ArrayList<>(); + @JsonIgnore + private DecodeEvent mChannelGrantEvent; + public DecodeConfigDMR() { } @@ -191,4 +195,32 @@ public class DecodeConfigDMR extends DecodeConfiguration { mTimeslotMap.add(timeslotFrequency); } + + @JsonIgnore + /** + * Original channel grant event. This is used for trunking systems to pass the original channel grant event to + * the decoder state on channel startup. + */ + public DecodeEvent getChannelGrantEvent() + { + return mChannelGrantEvent; + } + + /** + * Sets the original channel grant event. + * @param channelGrantEvent to set on the decoder state. + */ + public void setChannelGrantEvent(DecodeEvent channelGrantEvent) + { + mChannelGrantEvent = channelGrantEvent; + } + + @JsonIgnore + /** + * Indicates if this decode config has an original channel grant event to preload. + */ + public boolean hasChannelGrantEvent() + { + return mChannelGrantEvent != null; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java index bf773bf1..cb50a841 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRAbsoluteChannel.java @@ -74,4 +74,12 @@ public class DMRAbsoluteChannel extends DMRChannel return sb.toString(); } + + @Override + public DMRChannel getSisterTimeslot() + { + DMRAbsoluteChannel other = new DMRAbsoluteChannel(getChannelNumber(), getTimeslot() == 1 ? 2 : 1, + getDownlinkFrequency(), getUplinkFrequency()); + return other; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java index cc2040c9..da4917d3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java @@ -52,6 +52,12 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe mTimeslot = timeslot; } + /** + * Creates a channel of the same type, but for the alternate timeslot. + * @return sister timeslot channel + */ + public abstract DMRChannel getSisterTimeslot(); + @Override public Protocol getProtocol() { @@ -160,6 +166,15 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe mTimeslotFrequency = timeslotFrequency; } + /** + * TimeslotFrequency for this channel. + * @return timeslot frequency instance, or null. + */ + public TimeslotFrequency getTimeslotFrequency() + { + return mTimeslotFrequency; + } + @Override public void apply(List timeslotFrequencies) { diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLsn.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLsn.java index ceb74c33..649cd051 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLsn.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRLsn.java @@ -69,4 +69,25 @@ public class DMRLsn extends DMRChannel return ((getChannelNumber() - 1) * 2) + getTimeslot(); } + /** + * Creates a channel of the same type, but for the alternate timeslot. + * @return sister timeslot channel + */ + public DMRChannel getSisterTimeslot() + { + int lsn; + + if(getTimeslot() == 1) + { + lsn = getLsn() + 1; + } + else + { + lsn = getLsn() - 1; + } + + DMRLsn other = new DMRLsn(lsn); + other.setTimeslotFrequency(getTimeslotFrequency()); + return other; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRTier3Channel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRTier3Channel.java index b14b2a10..07efcd05 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRTier3Channel.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRTier3Channel.java @@ -55,4 +55,12 @@ public class DMRTier3Channel extends DMRChannel sb.append(" CHANID:").append(getChannelId()); return sb.toString(); } + + @Override + public DMRChannel getSisterTimeslot() + { + DMRTier3Channel other = new DMRTier3Channel(getChannelNumber(), getTimeslot() == 1 ? 2 : 1); + other.setTimeslotFrequency(getTimeslotFrequency()); + return other; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java b/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java deleted file mode 100644 index c9ecddc1..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/event/DMRChannelGrantEvent.java +++ /dev/null @@ -1,169 +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.module.decode.dmr.event; - -import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; -import io.github.dsheirer.module.decode.event.DecodeEventType; - -/** - * DMR Channel Grant Event. - * - * This event class is send traffic channel grant details to the traffic channel manager to allocate a traffic channel. - */ -public class DMRChannelGrantEvent extends DMRDecodeEvent -{ - private boolean mEncrypted; - - /** - * Constructs an instance - * @param decodeEventType for the event - * @param timestamp for the event - */ - public DMRChannelGrantEvent(DecodeEventType decodeEventType, long timestamp) - { - super(decodeEventType, timestamp); - } - - /** - * Indicates if the channel grant was for an encrypted call - */ - public boolean isEncrypted() - { - return mEncrypted; - } - - /** - * Sets the encrypte state for the call - * @param encrypted - */ - public void setEncrypted(boolean encrypted) - { - mEncrypted = encrypted; - } - - /** - * Creates a new decode event builder with the specified start timestamp. - * @param timeStart for the event - * @return builder - */ - public static DMRChannelGrantDecodeEventBuilder channelGrantBuilder(DecodeEventType decodeEventType, long timeStart) - { - return new DMRChannelGrantDecodeEventBuilder(decodeEventType, timeStart); - } - - /** - * Builder pattern for constructing decode events. - */ - public static class DMRChannelGrantDecodeEventBuilder - { - protected long mTimeStart; - protected long mDuration; - protected DecodeEventType mDecodeEventType; - protected IdentifierCollection mIdentifierCollection; - protected DMRChannel mChannel; - protected String mDetails; - protected boolean mEncrypted; - - /** - * Constructs a builder instance with the specified start time in milliseconds - * - * @param timeStart - */ - public DMRChannelGrantDecodeEventBuilder(DecodeEventType decodeEventType, long timeStart) - { - mDecodeEventType = decodeEventType; - mTimeStart = timeStart; - } - - /** - * Sets the encrypted state for the channel/timeslot - */ - public DMRChannelGrantDecodeEventBuilder encrypted(boolean encrypted) - { - mEncrypted = encrypted; - return this; - } - - /** - * Sets the duration value - * @param duration in milliseconds - */ - public DMRChannelGrantDecodeEventBuilder duration(long duration) - { - mDuration = duration; - return this; - } - - /** - * Sets the duration value using the end - start timestamps - * @param timestamp for end of event - */ - public DMRChannelGrantDecodeEventBuilder end(long timestamp) - { - mDuration = timestamp - mTimeStart; - return this; - } - - /** - * Sets the channel descriptor for this event - * @param channel - */ - public DMRChannelGrantDecodeEventBuilder channel(DMRChannel channel) - { - mChannel = channel; - return this; - } - - /** - * Sets the identifier collection. - * @param identifierCollection containing optional identifiers like TO, FROM, frequency and - * alias list configuration name. - */ - public DMRChannelGrantDecodeEventBuilder identifiers(IdentifierCollection identifierCollection) - { - mIdentifierCollection = identifierCollection; - return this; - } - - /** - * Sets the details for the event - * @param details - */ - public DMRChannelGrantDecodeEventBuilder details(String details) - { - mDetails = details; - return this; - } - - /** - * Builds the decode event - */ - public DMRChannelGrantEvent build() - { - DMRChannelGrantEvent decodeEvent = new DMRChannelGrantEvent(mDecodeEventType, mTimeStart); - decodeEvent.setChannelDescriptor(mChannel); - decodeEvent.setTimeslot(mChannel.getTimeslot()); - decodeEvent.setDetails(mDetails); - decodeEvent.setDuration(mDuration); - decodeEvent.setIdentifierCollection(mIdentifierCollection); - return decodeEvent; - } - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java index 66e6bfc4..7d638cd3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java @@ -34,7 +34,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTraff import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble; import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha; -import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxGroupVoiceChannelUpdate; +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.CapacityPlusCSBKO_60; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusDataRevertWindowAnnouncement; import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusDataRevertWindowGrant; @@ -278,8 +279,11 @@ public class CSBKMessageFactory case MOTOROLA_CAPPLUS_DATA_WINDOW_GRANT: csbk = new CapacityPlusDataRevertWindowGrant(pattern, message, cach, slotType, timestamp, timeslot); break; - case MOTOROLA_CAPMAX_GROUP_VOICE_CHANNEL_UPDATE: - csbk = new CapacityMaxGroupVoiceChannelUpdate(pattern, message, cach, slotType, timestamp, timeslot); + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE: + csbk = new CapacityMaxOpenModeVoiceChannelUpdate(pattern, message, cach, slotType, timestamp, timeslot); + break; + case MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE: + csbk = new CapacityMaxAdvantageModeVoiceChannelUpdate(pattern, message, cach, slotType, timestamp, timeslot); break; case MOTOROLA_CONPLUS_CSBKO_10: csbk = new ConnectPlusOTAAnnouncement(pattern, message, cach, slotType, timestamp, timeslot); 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 bb4f8606..e8044d40 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 @@ -78,8 +78,10 @@ public enum Opcode MOTOROLA_CONPLUS_DATA_WINDOW_GRANT(Vendor.MOTOROLA_CONNECT_PLUS, 29, "ENHANCED DATA REVERT WINDOW GRANT"), MOTOROLA_CAPMAX_GROUP_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, 16, "GROUP CHANNEL USER"), - MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "ALOHA"), - MOTOROLA_CAPMAX_GROUP_VOICE_CHANNEL_UPDATE(Vendor.MOTOROLA_CAPACITY_PLUS, 33, "TALKGROUP ACTIVE"), + MOTOROLA_CAPMAX_TALKER_ALIAS(Vendor.MOTOROLA_CAPACITY_PLUS, 20, "CAP MAX TALKER ALIAS"), + MOTOROLA_CAPMAX_ALOHA(Vendor.MOTOROLA_CAPACITY_PLUS, 25, "CAP MAX ALOHA"), + MOTOROLA_CAPMAX_CHANNEL_UPDATE_OPEN_MODE(Vendor.MOTOROLA_CAPACITY_PLUS, 33, "CAP MAX CHAN UPD OPEN MODE"), + MOTOROLA_CAPMAX_CHANNEL_UPDATE_ADVANTAGE_MODE(Vendor.MOTOROLA_CAPACITY_PLUS, 34, "CAP MAX CHAN UPD ADV MODE"), MOTOROLA_CAPPLUS_CALL_ALERT(Vendor.MOTOROLA_CAPACITY_PLUS, 31, "CALL ALERT"), MOTOROLA_CAPPLUS_CALL_ALERT_ACK(Vendor.MOTOROLA_CAPACITY_PLUS, 32, "CALL ALERT ACK"), diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java new file mode 100644 index 00000000..92249ef1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java @@ -0,0 +1,333 @@ +/* + * ***************************************************************************** + * 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.module.decode.dmr.message.data.csbk.motorola; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; +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.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.List; + +/** + * Motorola Capacity Max Advantage Mode - Voice Channel Update (Opcode 34 / 0x22) + * Note: it appears that Capacity Max Talkgroups in advantage mode are only 10-bits long in range of 0 - 1023. + */ +public class CapacityMaxAdvantageModeVoiceChannelUpdate extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] CHANNEL_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}; + private static final int[] TALKGROUP_CH1_TS1 = new int[]{28, 29, 30, 31, 32, 33, 34, 35, 36, 37}; + private static final int[] TALKGROUP_CH1_TS2 = new int[]{38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + private static final int[] CHANNEL_2 = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}; + private static final int[] TALKGROUP_CH2_TS1 = new int[]{60, 61, 62, 63, 64, 65, 66, 67, 68, 69}; + private static final int[] TALKGROUP_CH2_TS2 = new int[]{70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + private TalkgroupIdentifier mTalkgroupCH1TS1; + private TalkgroupIdentifier mTalkgroupCH1TS2; + private TalkgroupIdentifier mTalkgroupCH2TS1; + private TalkgroupIdentifier mTalkgroupCH2TS2; + private DMRTier3Channel mChannel1TS1; + private DMRTier3Channel mChannel1TS2; + private DMRTier3Channel mChannel2TS1; + private DMRTier3Channel mChannel2TS2; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + */ + public CapacityMaxAdvantageModeVoiceChannelUpdate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + if(hasRAS()) + { + sb.append(" RAS:").append(getBPTCReservedBits()); + } + + sb.append(" CSBK CAP-MAX MODE:ADVANTAGE CHANNEL UPDATE"); + + if(hasChannel1Timeslot1()) + { + sb.append(" TG1:").append(getTalkgroupCH1TS1()); + sb.append(" ON").append(getChannel1TS1()); + } + if(hasChannel1Timeslot2()) + { + sb.append(" TG2:").append(getTalkgroupCH1TS2()); + sb.append(" ON").append(getChannel1TS2()); + } + + if(hasChannel2Timeslot1()) + { + sb.append(" TG3:").append(getTalkgroupCH1TS1()); + sb.append(" ON").append(getChannel2TS1()); + } + if(hasChannel2Timeslot2()) + { + sb.append(" TG4:").append(getTalkgroupCH1TS2()); + sb.append(" ON").append(getChannel2TS2()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Indicates if this message contains activity on channel 1 timeslot 1. + */ + public boolean hasChannel1Timeslot1() + { + return getMessage().getInt(TALKGROUP_CH1_TS1) > 0; + } + + /** + * Indicates if this message contains activity on channel 1 timeslot 2. + */ + public boolean hasChannel1Timeslot2() + { + return getMessage().getInt(TALKGROUP_CH1_TS2) > 0; + } + + /** + * Indicates if this message contains activity on channel 2 timeslot 1. + */ + public boolean hasChannel2Timeslot1() + { + return getMessage().getInt(TALKGROUP_CH2_TS1) > 0; + } + + /** + * Indicates if this message contains activity on channel 2 timeslot 2. + */ + public boolean hasChannel2Timeslot2() + { + return getMessage().getInt(TALKGROUP_CH2_TS2) > 0; + } + + /** + * Talkgroup that is active on channel 1 timeslot 1. + */ + public TalkgroupIdentifier getTalkgroupCH1TS1() + { + if(mTalkgroupCH1TS1 == null) + { + mTalkgroupCH1TS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH1_TS1)); + } + + return mTalkgroupCH1TS1; + } + + /** + * Talkgroup that is active on channel 1 timeslot 2. + */ + public TalkgroupIdentifier getTalkgroupCH1TS2() + { + if(mTalkgroupCH1TS2 == null) + { + mTalkgroupCH1TS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH1_TS2)); + } + + return mTalkgroupCH1TS2; + } + + /** + * Talkgroup that is active on channel 2 timeslot 1. + */ + public TalkgroupIdentifier getTalkgroupCH2TS1() + { + if(mTalkgroupCH2TS1 == null) + { + mTalkgroupCH2TS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH2_TS1)); + } + + return mTalkgroupCH2TS1; + } + + /** + * Talkgroup that is active on channel 2 timeslot 2. + */ + public TalkgroupIdentifier getTalkgroupCH2TS2() + { + if(mTalkgroupCH2TS2 == null) + { + mTalkgroupCH2TS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_CH2_TS2)); + } + + return mTalkgroupCH2TS2; + } + + /** + * Channel 1 Timeslot 1. + */ + public DMRTier3Channel getChannel1TS1() + { + if(mChannel1TS1 == null) + { + mChannel1TS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_1), 1); + } + + return mChannel1TS1; + } + + /** + * Channel 1 Timeslot 2. + */ + public DMRTier3Channel getChannel1TS2() + { + if(mChannel1TS2 == null) + { + mChannel1TS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_1), 2); + } + + return mChannel1TS2; + } + + /** + * Channel 2 Timeslot 1. + */ + public DMRTier3Channel getChannel2TS1() + { + if(mChannel2TS1 == null) + { + mChannel2TS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_2), 1); + } + + return mChannel2TS1; + } + + /** + * Channel 2 Timeslot 2. + */ + public DMRTier3Channel getChannel2TS2() + { + if(mChannel2TS2 == null) + { + mChannel2TS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_2), 2); + } + + return mChannel2TS2; + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(hasChannel1Timeslot1()) + { + getChannel1TS1().apply(timeslotFrequencies); + } + if(hasChannel1Timeslot2()) + { + getChannel1TS2().apply(timeslotFrequencies); + } + if(hasChannel2Timeslot1()) + { + getChannel2TS1().apply(timeslotFrequencies); + } + if(hasChannel2Timeslot2()) + { + getChannel2TS2().apply(timeslotFrequencies); + } + } + + @Override + public int[] getLogicalChannelNumbers() + { + //Since both timeslots share the same LCN, we use just TS1's channel for both channel 1 and channel 2 + if(hasChannel1Timeslot1() && hasChannel2Timeslot1()) + { + return new int[]{getChannel1TS1().getChannelId(), getChannel2TS1().getChannelId()}; + } + else if(hasChannel1Timeslot1()) + { + return new int[]{getChannel1TS1().getChannelId()}; + } + else if(hasChannel2Timeslot1()) + { + return new int[]{getChannel2TS1().getChannelId()}; + } + return getChannel1TS1().getLogicalChannelNumbers(); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasChannel1Timeslot1()) + { + mIdentifiers.add(getTalkgroupCH1TS1()); + mIdentifiers.add(getChannel1TS1()); + } + + if(hasChannel1Timeslot2()) + { + mIdentifiers.add(getTalkgroupCH1TS2()); + mIdentifiers.add(getChannel1TS2()); + } + + if(hasChannel2Timeslot1()) + { + mIdentifiers.add(getTalkgroupCH2TS1()); + mIdentifiers.add(getChannel2TS1()); + } + + if(hasChannel2Timeslot2()) + { + mIdentifiers.add(getTalkgroupCH2TS2()); + mIdentifiers.add(getChannel2TS2()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxGroupVoiceChannelUpdate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxGroupVoiceChannelUpdate.java deleted file mode 100644 index a79f9562..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxGroupVoiceChannelUpdate.java +++ /dev/null @@ -1,153 +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.module.decode.dmr.message.data.csbk.motorola; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; -import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; -import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; -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.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.List; - -/** - * Motorola Capacity Max - Group Voice Channel Update (Opcode 33 / 0x21) - * - * Likely incomplete implementation. - */ -public class CapacityMaxGroupVoiceChannelUpdate extends CSBKMessage implements ITimeslotFrequencyReceiver -{ - private static final int[] CHANNEL_NUMBER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}; - private static final int[] TIMESLOT = new int[]{28}; //Logical guess - not seeing it set in the example Tier3 recording - - private static final int[] TALKGROUP = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; - private TalkgroupIdentifier mTalkgroup; - private DMRTier3Channel mChannel; - private List mIdentifiers; - - /** - * Constructs an instance - * - * @param syncPattern for the CSBK - * @param message bits - * @param cach for the DMR burst - * @param slotType for this message - * @param timestamp - * @param timeslot - */ - public CapacityMaxGroupVoiceChannelUpdate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) - { - super(syncPattern, message, cach, slotType, timestamp, timeslot); - } - - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - - if(!isValid()) - { - sb.append("[CRC-ERROR] "); - } - - sb.append("CC:").append(getSlotType().getColorCode()); - if(hasRAS()) - { - sb.append(" RAS:").append(getBPTCReservedBits()); - } - - sb.append(" CSBK CAP-MAX GROUP VOICE CHANNEL UPDATE TALKGROUP:").append(getTalkgroup()); - sb.append(" CHANNEL:").append(getChannel()); - sb.append(" MSG:").append(getMessage().toHexString()); - return sb.toString(); - } - - /** - * Talkgroup that is active on the channel. - */ - public TalkgroupIdentifier getTalkgroup() - { - if(mTalkgroup == null) - { - mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP)); - } - - return mTalkgroup; - } - - /** - * Channel the talkgroup is using. - * @return channel. - */ - public DMRTier3Channel getChannel() - { - if(mChannel == null) - { - mChannel = new DMRTier3Channel(getMessage().getInt(CHANNEL_NUMBER), getChannelTimeslot()); - } - - return mChannel; - } - - /** - * Timeslot - */ - private int getChannelTimeslot() - { - return getMessage().getInt(TIMESLOT) + 1; - } - - /** - * Assigns a timeslot frequency map for the DMR channel - * - * @param timeslotFrequencies that match the logical timeslots - */ - @Override - public void apply(List timeslotFrequencies) - { - getChannel().apply(timeslotFrequencies); - } - - @Override - public int[] getLogicalChannelNumbers() - { - return getChannel().getLogicalChannelNumbers(); - } - - @Override - public List getIdentifiers() - { - if(mIdentifiers == null) - { - mIdentifiers = new ArrayList<>(); - mIdentifiers.add(getTalkgroup()); - mIdentifiers.add(getChannel()); - } - - return mIdentifiers; - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java new file mode 100644 index 00000000..0e1df375 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java @@ -0,0 +1,233 @@ +/* + * ***************************************************************************** + * 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.module.decode.dmr.message.data.csbk.motorola; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; +import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; +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.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.List; + +/** + * Motorola Capacity Max Open System Mode - Voice Channel Update (Opcode 33 / 0x21) + * + * Opcode 33 and 34 are very similar. However, Opcode 33 appears to support full 24-bit group identifiers whereas + * Opcode 34 only supports 10-bit group identifiers. + *

+ * Likely incomplete implementation. + */ +public class CapacityMaxOpenModeVoiceChannelUpdate extends CSBKMessage implements ITimeslotFrequencyReceiver +{ + private static final int[] CHANNEL_NUMBER = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}; + private static final int[] TALKGROUP_TS1 = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}; + private static final int[] TALKGROUP_TS2 = new int[]{56, 57, 58, 59, 60, 61, 63, 64, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}; + private TalkgroupIdentifier mTalkgroupTS1; + private TalkgroupIdentifier mTalkgroupTS2; + private DMRTier3Channel mChannelTS1; + private DMRTier3Channel mChannelTS2; + private List mIdentifiers; + + /** + * Constructs an instance + * + * @param syncPattern for the CSBK + * @param message bits + * @param cach for the DMR burst + * @param slotType for this message + * @param timestamp + * @param timeslot + */ + public CapacityMaxOpenModeVoiceChannelUpdate(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + { + super(syncPattern, message, cach, slotType, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + + sb.append("CC:").append(getSlotType().getColorCode()); + if(hasRAS()) + { + sb.append(" RAS:").append(getBPTCReservedBits()); + } + + sb.append(" CSBK CAP-MAX MODE:OPEN-SYSTEM CHANNEL UPDATE"); + + if(hasTimeslot1()) + { + sb.append(" TG1:").append(getTalkgroupTS1()); + sb.append(" ON").append(getChannelTS1()); + } + if(hasTimeslot2()) + { + sb.append(" TG2:").append(getTalkgroupTS2()); + sb.append(" ON").append(getChannelTS2()); + } + + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Indicates if this message contains channel update for timeslot 1. + */ + public boolean hasTimeslot1() + { + return getMessage().getInt(TALKGROUP_TS1) > 0; + } + + /** + * Indicates if this message contains channel update for timeslot 2. + */ + public boolean hasTimeslot2() + { + return getMessage().getInt(TALKGROUP_TS2) > 0; + } + + /** + * Talkgroup that is active on the channel for timeslot 1. + */ + public TalkgroupIdentifier getTalkgroupTS1() + { + if(mTalkgroupTS1 == null) + { + mTalkgroupTS1 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_TS1)); + } + + return mTalkgroupTS1; + } + + /** + * Talkgroup that is active on the channel for timeslot 2. + */ + public TalkgroupIdentifier getTalkgroupTS2() + { + if(mTalkgroupTS2 == null) + { + mTalkgroupTS2 = DMRTalkgroup.create(getMessage().getInt(TALKGROUP_TS2)); + } + + return mTalkgroupTS2; + } + + /** + * Channel for timeslot 1 + */ + public DMRTier3Channel getChannelTS1() + { + if(mChannelTS1 == null) + { + mChannelTS1 = new DMRTier3Channel(getMessage().getInt(CHANNEL_NUMBER), 1); + } + + return mChannelTS1; + } + + /** + * Channel for timeslot 2 + */ + public DMRTier3Channel getChannelTS2() + { + if(mChannelTS2 == null) + { + mChannelTS2 = new DMRTier3Channel(getMessage().getInt(CHANNEL_NUMBER), 2); + } + + return mChannelTS2; + } + + /** + * Assigns a timeslot frequency map for the DMR channel + * + * @param timeslotFrequencies that match the logical timeslots + */ + @Override + public void apply(List timeslotFrequencies) + { + if(hasTimeslot1()) + { + getChannelTS1().apply(timeslotFrequencies); + } + if(hasTimeslot2()) + { + getChannelTS2().apply(timeslotFrequencies); + } + } + + @Override + public int[] getLogicalChannelNumbers() + { + boolean ts1 = hasTimeslot1(); + boolean ts2 = hasTimeslot2(); + + if(ts1 && ts2) + { + return new int[]{getChannelTS1().getChannelNumber(), getChannelTS2().getChannelNumber()}; + } + else if(ts1) + { + return new int[]{getChannelTS1().getChannelNumber()}; + } + else if(ts2) + { + return new int[]{getChannelTS2().getChannelNumber()}; + } + + return new int[0]; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + + if(hasTimeslot1()) + { + mIdentifiers.add(getChannelTS1()); + mIdentifiers.add(getTalkgroupTS1()); + } + if(hasTimeslot2()) + { + mIdentifiers.add(getChannelTS2()); + mIdentifiers.add(getTalkgroupTS2()); + } + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java index 0e98ea91..18922ded 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java @@ -37,6 +37,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMe import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias; +import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; @@ -141,6 +143,12 @@ public class LCMessageFactory case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER: flc = new CapacityMaxVoiceChannelUser(message, timestamp, timeslot); break; + case FULL_CAPACITY_MAX_TALKER_ALIAS: + flc = new CapacityMaxTalkerAlias(message, timestamp, timeslot); + break; + case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: + flc = new CapacityMaxTalkerAliasContinuation(message, timestamp, timeslot); + break; case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER: flc = new CapacityPlusEncryptedVoiceChannelUser(message, timestamp, timeslot); break; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java index 798a3ceb..a6fe7ed5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java @@ -46,6 +46,8 @@ public enum LCOpcode FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 0, "GROUP VOICE CHANNEL USER"), FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 4, "WAN GROUP VOICE CHANNEL USER"), FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 16, "CAPMAX GROUP VOICE CHANNEL USER"), + FULL_CAPACITY_MAX_TALKER_ALIAS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 20, "CAPMAX TALKER ALIAS"), + FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION(Vendor.MOTOROLA_CAPACITY_PLUS, true, 21, "CAPMAX TALKER ALIAS CONTINUATION"), //Observed on Cap+ Multi-Site System during an encrypted voice call FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"), //Observed on Cap+ Multi-Site System during an encrypted voice call diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java new file mode 100644 index 00000000..ef8b0ad8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java @@ -0,0 +1,92 @@ +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.DmrTalkerAliasIdentifier; + +import java.util.ArrayList; +import java.util.List; + +/** + * Capacity Max Talker Alias (FLCO 20 / FID 0x10) + *

+ * See also: FLCO 21 - Talker Alias Continuation + */ +public class CapacityMaxTalkerAlias extends CapacityPlusVoiceChannelUser +{ + private static final int[] ENCODING_TYPE = new int[]{16, 17, 18}; + private static final int[] LENGTH = new int[]{19, 20, 21, 22}; + private static final int ALIAS_START = 24; + private static final int ALIAS_END = ALIAS_START + (6 * 8); //6 characters? + private DmrTalkerAliasIdentifier mTalkerAliasIdentifier; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + * @param timestamp + * @param timeslot + */ + public CapacityMaxTalkerAlias(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + public int getLength() + { + return getMessage().getInt(LENGTH); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + if(isEncrypted()) + { + sb.append(" *ENCRYPTED*"); + } + if(isReservedBitSet()) + { + sb.append(" *RESERVED-BIT*"); + } + + sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS:").append(getTalkerAliasIdentifier()); + sb.append(" LENGTH:").append(getLength()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Talker alias identifier + * + * @return identifier + */ + public DmrTalkerAliasIdentifier getTalkerAliasIdentifier() + { + if(mTalkerAliasIdentifier == null) + { + String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim(); + mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias); + } + + return mTalkerAliasIdentifier; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkerAliasIdentifier()); + } + + return mIdentifiers; + } +} 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 new file mode 100644 index 00000000..53b2de63 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java @@ -0,0 +1,81 @@ +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 java.util.ArrayList; +import java.util.List; + +/** + * Capacity Max Talker Alias Continuation (FLCO 21 / FID 0x10) + */ +public class CapacityMaxTalkerAliasContinuation extends CapacityPlusVoiceChannelUser +{ + private static final int ALIAS_START = 16; + private static final int ALIAS_END = ALIAS_START + (7 * 8); + private DmrTalkerAlias2Identifier mTalkerAliasIdentifier; + private List mIdentifiers; + + /** + * Constructs an instance. + * + * @param message for the link control payload + * @param timestamp + * @param timeslot + */ + public CapacityMaxTalkerAliasContinuation(CorrectedBinaryMessage message, long timestamp, int timeslot) + { + super(message, timestamp, timeslot); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("[CRC-ERROR] "); + } + if(isEncrypted()) + { + sb.append(" *ENCRYPTED*"); + } + if(isReservedBitSet()) + { + sb.append(" *RESERVED-BIT*"); + } + + sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS CONTINUED:").append(getTalkerAliasIdentifier()); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Talker alias identifier + * @return identifier + */ + public DmrTalkerAlias2Identifier getTalkerAliasIdentifier() + { + if(mTalkerAliasIdentifier == null) + { + String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim(); + mTalkerAliasIdentifier = DmrTalkerAlias2Identifier.create(alias); + } + + return mTalkerAliasIdentifier; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkerAliasIdentifier()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java index b9fa8ca8..6568e6b3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/shorty/ShortLCMessage.java @@ -88,7 +88,8 @@ public abstract class ShortLCMessage extends LCMessage implements IMessage { Vendor vendor = null; - if(binaryMessage.get(VENDOR_FLAG)) + //If the first bit is set, consider this a vendor variant, unless the opcode is 10 for Connect Plus Control Channel. + if(binaryMessage.get(VENDOR_FLAG) && binaryMessage.getInt(OPCODE) != 10) { vendor = Vendor.fromValue(binaryMessage.getInt(VENDOR)); } 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 8e0d59eb..3ce817b7 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 @@ -148,6 +148,15 @@ public enum DecodeEventType DATA_CALLS.contains(this) || REGISTRATION.contains(this); } + /** + * Indicates if this is a call event. + * @return true if this is a call event type. + */ + public boolean isCallEvent() + { + return VOICE_CALLS.contains(this) || VOICE_CALLS_ENCRYPTED.contains(this) || DATA_CALLS.contains(this); + } + /** * Label or pretty value for the element * @return label