#1787 Adds CapMax talker alias continuation FLCO. Resolves issue with Con+ control channel SLCO message detection. (#1796)

#1787 Adds Capacity Max talker alias FLCO.  Fixes standard FLC Talker Alias using ISO7 format.  Updates channel details to list the tier 3 neighbors in the details.
#1787 Updates DMR capacity max event handling to better show events.  Adds support for voice channel update for both Open System and Advantage modes.  May cause issues with Cap+ or Con+ due to changes ... needs more testing.
#1787 DMR capacity max group voice update handling.

Co-authored-by: sheirerd <sheirerd@ainfosec.com>
This commit is contained in:
Denny Sheirer
2024-01-18 11:48:25 -05:00
committed by GitHub
parent 9d03c151af
commit e2a0325d07
27 changed files with 1598 additions and 829 deletions

View File

@@ -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));

View File

@@ -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<DMRChannel,DecodeEvent> 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<Identifier> 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<Identifier> 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:

View File

@@ -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<SiteIdentifier> mNeighborSites = new ArrayList<>();
private Map<Integer,AdjacentSiteInformation> 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<Integer,AdjacentSiteInformation> 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");
}
}

View File

@@ -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<Channel> mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>();
private List<Channel> mManagedTrafficChannels;
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
private Queue<Channel> mAvailableTrafficChannels = new ConcurrentLinkedQueue<>();
private List<Channel> mAllocatedTrafficChannels;
private Map<Long,Channel> mAllocatedChannelFrequencyMap = new ConcurrentHashMap<>();
private ReentrantLock mLock = new ReentrantLock();
private Map<Long,IDecodeEvent> mCallEventsTS1 = new ConcurrentHashMap<>();
private Map<Long,IDecodeEvent> mCallEventsTS2 = new ConcurrentHashMap<>();
private Listener<ChannelEvent> mChannelEventListener;
private Listener<IDecodeEvent> 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<IMessage> 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<Channel> 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<Long,IDecodeEvent> 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<Channel> 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<Channel> 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<DMRChannel> channelsToRemove = new ArrayList<>();
mLock.lock();
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> 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<DMRChannel> channelsToUpdate = new ArrayList<>();
mLock.lock();
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> 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<Long,Channel> entry: mAllocatedTrafficChannelFrequencyMap.entrySet())
mLock.lock();
try
{
if(channel == entry.getValue() && entry.getKey() != null)
for(Map.Entry<Long,Channel> 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<Long,Channel> entry: mAllocatedTrafficChannelFrequencyMap.entrySet())
mLock.lock();
try
{
if(channel == entry.getValue() && entry.getKey() != null)
long frequencyToUpdate = 0;
for(Map.Entry<Long,Channel> 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;
}

View File

@@ -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<TimeslotFrequency> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<TimeslotFrequency> timeslotFrequencies)
{

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}
}

View File

@@ -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);

View File

@@ -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"),

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<TimeslotFrequency> 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<Identifier> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<TimeslotFrequency> timeslotFrequencies)
{
getChannel().apply(timeslotFrequencies);
}
@Override
public int[] getLogicalChannelNumbers()
{
return getChannel().getLogicalChannelNumbers();
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
mIdentifiers.add(getChannel());
}
return mIdentifiers;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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.
* <p>
* 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<Identifier> 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<TimeslotFrequency> 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<Identifier> 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;
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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)
* <p>
* 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<Identifier> 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<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

@@ -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<Identifier> 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<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

@@ -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));
}

View File

@@ -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