#2089 Motorola P25P2 TDMA data channel support (#2090)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2024-11-12 02:22:22 -05:00
committed by GitHub
parent 552d3a6ce9
commit 5c9c3fa503
19 changed files with 751 additions and 8 deletions

View File

@@ -36,6 +36,7 @@ public class P25TrafficChannelEventTracker
{
private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class);
private static final long STALE_EVENT_THRESHOLD_MS = 2000;
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
private P25ChannelGrantEvent mEvent;
private boolean mStarted = false;
private boolean mComplete = false;
@@ -74,6 +75,14 @@ public class P25TrafficChannelEventTracker
return timestamp - getEvent().getTimeStart() > STALE_EVENT_THRESHOLD_MS;
}
/**
* Indicates if the TDMA data channel duration exceeds the threshold (15 seconds)
*/
public boolean exceedsMaxTDMADataDuration()
{
return getEvent().getDuration() > MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS;
}
/**
* Adds the identifier to the tracked event if the event's identifier collection does not already have it.
* @param identifier to add

View File

@@ -58,6 +58,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.Net
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacOpcode;
import io.github.dsheirer.module.decode.p25.reference.DataServiceOptions;
import io.github.dsheirer.module.decode.p25.reference.ServiceOptions;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
@@ -528,6 +529,99 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
}
}
/**
* Process a TDMA data channel grant.
* @param channel for data.
* @param timestamp of the event
*/
public void processP2DataChannel(APCO25Channel channel, long timestamp)
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
if(frequency > 0)
{
mLock.lock();
try
{
P25TrafficChannelEventTracker trackerTS1 = getTrackerRemoveIfStale(channel.getDownlinkFrequency(),
P25P1Message.TIMESLOT_1, timestamp);
if(trackerTS1 != null && trackerTS1.exceedsMaxTDMADataDuration())
{
removeTracker(frequency, P25P1Message.TIMESLOT_1);
trackerTS1 = null;
}
if(trackerTS1 == null)
{
P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(DecodeEventType.DATA_CALL,
timestamp, new DataServiceOptions(0))
.channelDescriptor(channel)
.details("TDMA PHASE 2 DATA CHANNEL ACTIVE")
.identifiers(new IdentifierCollection())
.timeslot(P25P1Message.TIMESLOT_1)
.build();
trackerTS1 = new P25TrafficChannelEventTracker(continuationGrantEvent);
addTracker(trackerTS1, frequency, P25P1Message.TIMESLOT_1);
}
//update the ending timestamp so that the duration value is correctly calculated
trackerTS1.updateDurationTraffic(timestamp);
broadcast(trackerTS1);
//Even though we have a tracked event, the initial channel grant may have been rejected. Check to
// see if there is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !mIgnoreDataCalls &&
(getCurrentControlFrequency() != frequency))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
if(trafficChannel != null)
{
requestTrafficChannelStart(trafficChannel, channel, new IdentifierCollection(), timestamp);
}
else
{
trackerTS1.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED);
}
}
P25TrafficChannelEventTracker trackerTS2 = getTrackerRemoveIfStale(channel.getDownlinkFrequency(),
P25P1Message.TIMESLOT_2, timestamp);
if(trackerTS2 != null && trackerTS2.exceedsMaxTDMADataDuration())
{
removeTracker(frequency, P25P1Message.TIMESLOT_2);
trackerTS2 = null;
}
if(trackerTS2 == null)
{
P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(DecodeEventType.DATA_CALL,
timestamp, new DataServiceOptions(0))
.channelDescriptor(channel)
.details("TDMA PHASE 2 DATA CHANNEL ACTIVE")
.identifiers(new IdentifierCollection())
.timeslot(P25P1Message.TIMESLOT_2)
.build();
trackerTS2 = new P25TrafficChannelEventTracker(continuationGrantEvent);
addTracker(trackerTS2, frequency, P25P1Message.TIMESLOT_2);
}
//update the ending timestamp so that the duration value is correctly calculated
trackerTS2.updateDurationTraffic(timestamp);
broadcast(trackerTS1);
}
finally
{
mLock.unlock();
}
}
}
/**
* Starts a tracked event and updates the duration for a tracked event.
*

View File

@@ -118,6 +118,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.L3HarrisGroupRegroupExplicitEncryptionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaAcknowledgeResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaDenyResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelUpdate;
@@ -1518,6 +1519,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
break;
case MOTOROLA_OSP_QUEUED_RESPONSE:
processTSBKQueuedResponse(tsbk);
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
processTSBKActiveTDMADataChannel(tsbk);
break;
default:
// if(!tsbk.getOpcode().name().startsWith("ISP"))
// {
@@ -1531,6 +1536,19 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
}
}
/**
* TSBK Motorola TDMA data channel is active.
* @param tsbk with channel
*/
private void processTSBKActiveTDMADataChannel(TSBKMessage tsbk)
{
if(tsbk instanceof MotorolaExplicitTDMADataChannelAnnouncement tdma && tdma.hasChannel())
{
mTrafficChannelManager.processP2DataChannel(tdma.getChannel(), tsbk.getTimestamp());
mNetworkConfigurationMonitor.process(tsbk);
}
}
/**
* TSBK Status messaging
*/

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast;
@@ -38,6 +39,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNe
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaBaseStationId;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.AdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.NetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.RFSSStatusBroadcast;
@@ -79,8 +81,9 @@ public class P25P1NetworkConfigurationMonitor
//Current Site Secondary Control Channels
private Map<String,IChannelDescriptor> mSecondaryControlChannels = new TreeMap<>();
//Current Site Data Channel
//Current Site Data Channel(s)
private SNDCPDataChannelAnnouncementExplicit mSNDCPDataChannel;
private Map<APCO25Channel, MotorolaExplicitTDMADataChannelAnnouncement> mTDMADataChannelMap = new HashMap<>();
//Current Site Services
private SystemServiceBroadcast mTSBKSystemServiceBroadcast;
@@ -178,6 +181,12 @@ public class P25P1NetworkConfigurationMonitor
mMotorolaBaseStationId = (MotorolaBaseStationId)tsbk;
}
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
if(tsbk instanceof MotorolaExplicitTDMADataChannelAnnouncement tdma && tdma.hasChannel())
{
mTDMADataChannelMap.put(tdma.getChannel(), tdma);
}
break;
}
}
@@ -442,11 +451,21 @@ public class P25P1NetworkConfigurationMonitor
if(mSNDCPDataChannel != null)
{
sb.append(" CURRENT DATA CHANNEL:").append(mSNDCPDataChannel.getChannel());
sb.append(" CURRENT FDMA DATA CHANNEL:").append(mSNDCPDataChannel.getChannel());
sb.append(" DOWNLINK:").append(mSNDCPDataChannel.getChannel().getDownlinkFrequency());
sb.append(" UPLINK:").append(mSNDCPDataChannel.getChannel().getUplinkFrequency()).append("\n");
}
if(!mTDMADataChannelMap.isEmpty())
{
for(Map.Entry<APCO25Channel, MotorolaExplicitTDMADataChannelAnnouncement> entry: mTDMADataChannelMap.entrySet())
{
sb.append(" ACTIVE TDMA DATA CHANNEL:").append(entry.getKey());
sb.append(" DOWNLINK:").append(entry.getKey().getDownlinkFrequency());
sb.append(" UPLINK:").append(entry.getKey().getUplinkFrequency()).append("\n");
}
}
if(mMotorolaBaseStationId != null)
{
sb.append(" STATION ID/LICENSE: ").append(mMotorolaBaseStationId.getCWID()).append("\n");

View File

@@ -181,7 +181,7 @@ public enum Opcode
MOTOROLA_OSP_BASE_STATION_ID(11, "CCH BASE STAT ID", "CONTROL CHANNEL BASE STATION ID"),
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN(14, "CCH PLND SHUTDWN", "CONTROL CHANNEL PLANNED SHUTDOWN"),
MOTOROLA_OSP_OPCODE_15(15, "MOTOROLA OPCODE 15", "MOTOROLA OPCODE 15"),
//Opcode 22 - observed on PA-STARNET VHF Phase 1 CC site: 1690423FFFFFFFFF0000D458 & 9690423FFFFFFFFF0000306C
MOTOROLA_OSP_TDMA_DATA_CHANNEL(22, "MOTOROLA TDMA DATA CHANNEL", "MOTOROLA TDMA DATA CHANNEL"),
MOTOROLA_OSP_UNKNOWN(-1, "MOTOROLA OSP UNKNOWN OPCODE", "MOTOROLA OSP UNKNOWN OPCODE"),
//Vendor: L3Harris, Inbound Service Packet (ISP)
@@ -299,7 +299,7 @@ public enum Opcode
MOTOROLA_OSP_GROUP_REGROUP_DELETE, MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT,
MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_UPDATE, MOTOROLA_OSP_TRAFFIC_CHANNEL_ID,
MOTOROLA_OSP_DENY_RESPONSE, MOTOROLA_OSP_SYSTEM_LOADING, MOTOROLA_OSP_BASE_STATION_ID,
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN, MOTOROLA_OSP_UNKNOWN);
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN, MOTOROLA_OSP_TDMA_DATA_CHANNEL, MOTOROLA_OSP_UNKNOWN);
/**
* Harris opcodes
@@ -479,6 +479,8 @@ public enum Opcode
return MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN;
case 0x0F:
return MOTOROLA_OSP_OPCODE_15;
case 0x16:
return MOTOROLA_OSP_TDMA_DATA_CHANNEL;
default:
return MOTOROLA_OSP_UNKNOWN;
}

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.Mot
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaBaseStationId;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaDenyResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaEmergencyAlarmActivation;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupAddCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelGrant;
@@ -441,6 +442,9 @@ public class TSBKMessageFactory
case MOTOROLA_OSP_OPCODE_15:
tsbk = new MotorolaOpcode15(dataUnitID, message, nac, timestamp);
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
tsbk = new MotorolaExplicitTDMADataChannelAnnouncement(dataUnitID, message, nac, timestamp);
break;
case MOTOROLA_OSP_UNKNOWN:
tsbk = new UnknownMotorolaOSPMessage(dataUnitID, message, nac, timestamp);
break;

View File

@@ -0,0 +1,143 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25ExplicitChannel;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.VendorOSPMessage;
import java.util.Collections;
import java.util.List;
/**
* Motorola TSBK Opcode 22 (0x16). This seems to be a TDMA data channel announcement message. A curious thing is the
* frequency band for the channel number is an FDMA band. I only have two examples of this, but in both examples
* the frequency band and channel number align with known frequencies for the system and site and in one of those cases
* it was an actual TDMA data channel, so this may or may not be correct.
*
* Example: PA-STARNET VHF control channel - not announcing a dedicated TDMA channel (15-4095).
* 1690423FFFFFFFFF0000D458
* 9690423FFFFFFFFF0000306C
*
* Example: Victoria Radio Network (VRN) Metro Mobile Radio - UHF control channel with a dedicated TDMA data channel.
* https://www.radioreference.com/db/sid/7197 Site 1
* 1690C23F3060FFFF00001199 0x3060 = 3-96 = 420.6125 MHz
* 9690C23F3060FFFF0000F5AD
* 1690C23F803EFFFF000030AF 0x803E = 8-62 = 467.9000 MHz.
* 9690C23F803EFFFF0000D49B
*
* This messages correlates with Motorola TDMA MAC OPCODE 139 which contained both sequences x3060 and x803E that were
* carried in separate Opcode 22 messages in the VRN control channel examples, but transmitted just this single message
* repeatedly on the dedicated TDMA data channel. TDMA Data Channel was active on 420.6125 MHz.
* 8B900FFF803EFF3060FF3060FF3060
* .... .... .... ....
*/
public class MotorolaExplicitTDMADataChannelAnnouncement extends VendorOSPMessage implements IFrequencyBandReceiver
{
private static final IntField DOWNLINK_FREQUENCY_BAND = IntField.length4(OCTET_4_BIT_32);
private static final IntField DOWNLINK_CHANNEL_NUMBER = IntField.length12(OCTET_4_BIT_32 + 4);
private static final IntField UPLINK_FREQUENCY_BAND = IntField.length4(OCTET_6_BIT_48);
private static final IntField UPLINK_CHANNEL_NUMBER = IntField.length12(OCTET_6_BIT_48 + 4);
private APCO25Channel mChannel;
/**
* Constructs a TSBK from the binary message sequence.
*/
public MotorolaExplicitTDMADataChannelAnnouncement(P25P1DataUnitID dataUnitId, CorrectedBinaryMessage message, int nac, long timestamp)
{
super(dataUnitId, message, nac, timestamp);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
if(hasChannel())
{
sb.append(" ACTIVE CHAN:").append(getChannel());
sb.append(" FREQ:").append(getChannel().getDownlinkFrequency());
}
else
{
sb.append(" NOT ACTIVE");
}
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* TDMA data channel.
*/
public APCO25Channel getChannel()
{
if(mChannel == null && hasChannel())
{
if(hasUplink())
{
mChannel = APCO25ExplicitChannel.create(getInt(DOWNLINK_FREQUENCY_BAND), getInt(DOWNLINK_CHANNEL_NUMBER),
getInt(UPLINK_FREQUENCY_BAND), getInt(UPLINK_CHANNEL_NUMBER));
}
else
{
mChannel = APCO25Channel.create(getInt(DOWNLINK_FREQUENCY_BAND), getInt(DOWNLINK_CHANNEL_NUMBER));
}
}
return mChannel;
}
/**
* Indicates if the message has a data channel
*/
public boolean hasChannel()
{
return getMessage().getInt(DOWNLINK_FREQUENCY_BAND) != 0xF && getMessage().getInt(DOWNLINK_CHANNEL_NUMBER) != 0xFFF;
}
/**
* Indicates if the message has an uplink frequency band and channel number.
*/
private boolean hasUplink()
{
return getMessage().getInt(UPLINK_FREQUENCY_BAND) != 0xF && getMessage().getInt(UPLINK_CHANNEL_NUMBER) != 0xFFF;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public List<IChannelDescriptor> getChannels()
{
if(hasChannel())
{
return List.of(getChannel());
}
return List.of();
}
}

View File

@@ -126,6 +126,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.DatchTimeslot;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
import io.github.dsheirer.protocol.Protocol;
import java.util.Collections;
@@ -240,6 +241,11 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
//If we're tracking the call event, update the duration on it
mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp());
}
//Motorola TDMA data channel.
else if(message instanceof DatchTimeslot)
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot()));
}
else if(message instanceof EncryptionSynchronizationSequence ess)
{
//We don't send any state events for this message since it can only occur in conjunction with
@@ -523,6 +529,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case MOTOROLA_89_GROUP_REGROUP_DELETE:
processDynamicRegrouping(message, mac);
break;
case MOTOROLA_8B_TDMA_DATA_CHANNEL:
break;
case MOTOROLA_91_TALKER_ALIAS_HEADER:
//Unknown
break;

View File

@@ -36,6 +36,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractSignalingTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.DatchTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Timeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot;
import io.github.dsheirer.sample.Listener;
@@ -98,7 +99,11 @@ public class P25P2MessageProcessor implements Listener<IMessage>
for(Timeslot timeslot: sff.getTimeslots())
{
if(timeslot instanceof AbstractSignalingTimeslot)
if(timeslot instanceof DatchTimeslot)
{
mMessageListener.receive(timeslot);
}
else if(timeslot instanceof AbstractSignalingTimeslot)
{
AbstractSignalingTimeslot ast = (AbstractSignalingTimeslot)timeslot;

View File

@@ -25,7 +25,6 @@ import java.util.EnumSet;
*/
public enum DataUnitID
{
VOICE_4(0, 0x00,"VOICE-4"),
RESERVED_1(1, 0x17, "RESERVED 1"),
RESERVED_2(2, 0x2E, "RESERVED 2"),
@@ -36,7 +35,7 @@ public enum DataUnitID
RESERVED_7(7, 0x72, "RESERVED 7"),
RESERVED_8(8, 0x8D, "RESERVED 8"),
SCRAMBLED_FACCH(9, 0x9A, "FACCH-S"),
RESERVED_A(10, 0xA3, "RESERVED 10"),
SCRAMBLED_DATCH(10, 0xA3, "DATCH-S"), //Motorola APX-Next TDMA Data Channel
RESERVED_B(11, 0xB4, "RESERVED 11"),
UNSCRAMBLED_SACCH(12, 0xC6, "SACCH-U"),
UNSCRAMBLED_LCCH(13, 0xD1, "LOCCH-U"),
@@ -133,7 +132,7 @@ public enum DataUnitID
case 0x9A:
return SCRAMBLED_FACCH;
case 0xA3:
return RESERVED_A;
return SCRAMBLED_DATCH;
case 0xB4:
return RESERVED_B;
case 0xC6:

View File

@@ -143,8 +143,10 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTDMADataChannel;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasDataBlock;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaUnknownOpcode135;
import io.github.dsheirer.module.decode.p25.reference.Vendor;
import java.util.ArrayList;
import java.util.List;
@@ -587,8 +589,12 @@ public class MacMessageFactory
return new MotorolaGroupRegroupVoiceChannelUpdate(message, offset);
case MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND:
return new MotorolaGroupRegroupExtendedFunctionCommand(message, offset);
case MOTOROLA_87_UNKNOWN_OPCODE_135:
return new MotorolaUnknownOpcode135(message, offset);
case MOTOROLA_89_GROUP_REGROUP_DELETE:
return new MotorolaGroupRegroupDeleteCommand(message, offset);
case MOTOROLA_8B_TDMA_DATA_CHANNEL:
return new MotorolaTDMADataChannel(message, offset);
case MOTOROLA_8F_ACTIVE_GROUP_RADIOS_143:
return new MotorolaActiveGroupRadiosOpcode143_x8F(message, offset);
case MOTOROLA_BF_ACTIVE_GROUP_RADIOS_191:

View File

@@ -118,7 +118,9 @@ public enum MacOpcode
MOTOROLA_82_ACTIVE_GROUP_RADIOS_130(Vendor.MOTOROLA, 130, "MOTOROLA ACTIVE GROUP RADIOS 130", Integer.MIN_VALUE),
MOTOROLA_83_GROUP_REGROUP_VOICE_CHANNEL_UPDATE(Vendor.MOTOROLA, 131, "MOTOROLA INDIVIDUAL REGROUP ACTIVE", 7),
MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND(Vendor.MOTOROLA, 133, "MOTOROLA GROUP REGROUP", 11),
MOTOROLA_87_UNKNOWN_OPCODE_135(Vendor.MOTOROLA, 135, "MOTOROLA UNKNOWN OPCODE 135", Integer.MIN_VALUE),
MOTOROLA_89_GROUP_REGROUP_DELETE(Vendor.MOTOROLA, 137, "MOTOROLA GROUP REGROUP DELETE", 17),
MOTOROLA_8B_TDMA_DATA_CHANNEL(Vendor.MOTOROLA, 139, "MOTOROLA TDMA DATA CHANNEL", Integer.MIN_VALUE),
MOTOROLA_8F_ACTIVE_GROUP_RADIOS_143(Vendor.MOTOROLA, 143, "MOTOROLA ACTIVE GROUP RADIOS 143", Integer.MIN_VALUE),
MOTOROLA_BF_ACTIVE_GROUP_RADIOS_191(Vendor.MOTOROLA, 191, "MOTOROLA ACTIVE GROUP RADIOS 191", Integer.MIN_VALUE),
//Opcode 144 uses STANDARD vendor ID for some reason.

View File

@@ -70,4 +70,12 @@ public abstract class MacStructureVariableLength extends MacStructure
{
return getLength(getMessage(), getOffset());
}
/**
* Message fragment for this variable length message.
*/
public CorrectedBinaryMessage getSubMessage()
{
return getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8));
}
}

View File

@@ -0,0 +1,269 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola TDMA data channel active announcement. Opcode 139.
*
* This messages correlates with Motorola FDMA TSBK OPCODE 22 which contained the same sequences x3060 and x803E and
* was transmitted on the control channel for this TDMA data channel and seems to be frequency band and channel number
* references.
* 8B900FFF803EFF3060FF3060FF3060
*
* Example: Victoria Radio Network (VRN) - UHF control channel with a dedicated TDMA data channel.
* It toggled through all four examples.
* 1690C23F3060FFFF00001199
* 9690C23F3060FFFF0000F5AD
* 1690C23F803EFFFF000030AF
* 9690C23F803EFFFF0000D49B
*/
public class MotorolaTDMADataChannel extends MacStructureVendor implements IFrequencyBandReceiver
{
private static final IntField UNKNOWN_1 = IntField.length8(OCTET_4_BIT_24);
private static final IntField FREQUENCY_BAND_1 = IntField.length4(OCTET_5_BIT_32);
private static final IntField CHANNEL_NUMBER_1 = IntField.length12(OCTET_5_BIT_32 + 4);
private static final IntField UNKNOWN_2 = IntField.length8(OCTET_7_BIT_48);
private static final IntField FREQUENCY_BAND_2 = IntField.length4(OCTET_8_BIT_56);
private static final IntField CHANNEL_NUMBER_2 = IntField.length12(OCTET_8_BIT_56 + 4);
private static final IntField UNKNOWN_3 = IntField.length8(OCTET_10_BIT_72);
private static final IntField FREQUENCY_BAND_3 = IntField.length4(OCTET_11_BIT_80);
private static final IntField CHANNEL_NUMBER_3 = IntField.length12(OCTET_11_BIT_80 + 4);
private static final IntField UNKNOWN_4 = IntField.length8(OCTET_13_BIT_96);
private static final IntField FREQUENCY_BAND_4 = IntField.length4(OCTET_14_BIT_104);
private static final IntField CHANNEL_NUMBER_4 = IntField.length12(OCTET_14_BIT_104 + 4);
private APCO25Channel mChannel1;
private APCO25Channel mChannel2;
private APCO25Channel mChannel3;
private APCO25Channel mChannel4;
/**
* Constructs the message
*
* @param message containing the message bits
* @param offset into the message for this structure
*/
public MotorolaTDMADataChannel(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Textual representation of this message
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getOpcode());
if(hasChannel1())
{
sb.append(" ACTIVE CHAN1:").append(getChannel1());
if(hasChannel2())
{
sb.append(" CHAN2:").append(getChannel2());
if(hasChannel3())
{
sb.append(" CHAN3:").append(getChannel3());
if(hasChannel1())
{
sb.append(" CHAN4:").append(getChannel4());
}
}
}
}
else
{
sb.append(" NOT ACTIVE");
}
sb.append(" MSG:").append(getSubMessage().toHexString());
return sb.toString();
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel1()
{
return getInt(CHANNEL_NUMBER_1) != 0xF && getInt(FREQUENCY_BAND_1) != 0xFFF;
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel1()
{
if(mChannel1 == null)
{
mChannel1 = APCO25Channel.create(getInt(FREQUENCY_BAND_1), getInt(CHANNEL_NUMBER_1));
}
return mChannel1;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel2()
{
return getInt(CHANNEL_NUMBER_2) != 0xF && getInt(FREQUENCY_BAND_2) != 0xFFF &&
getInt(FREQUENCY_BAND_2) != getInt(FREQUENCY_BAND_1) &&
getInt(CHANNEL_NUMBER_2) != getInt(CHANNEL_NUMBER_1);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel2()
{
if(mChannel2 == null)
{
mChannel2 = APCO25Channel.create(getInt(FREQUENCY_BAND_2), getInt(CHANNEL_NUMBER_2));
}
return mChannel2;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel3()
{
return getInt(CHANNEL_NUMBER_3) != 0xF && getInt(FREQUENCY_BAND_3) != 0xFFF &&
getInt(FREQUENCY_BAND_3) != getInt(FREQUENCY_BAND_2) &&
getInt(CHANNEL_NUMBER_3) != getInt(CHANNEL_NUMBER_2);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel3()
{
if(mChannel3 == null)
{
mChannel3 = APCO25Channel.create(getInt(FREQUENCY_BAND_3), getInt(CHANNEL_NUMBER_3));
}
return mChannel3;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel4()
{
return getInt(CHANNEL_NUMBER_4) != 0xF && getInt(FREQUENCY_BAND_4) != 0xFFF &&
getInt(FREQUENCY_BAND_4) != getInt(FREQUENCY_BAND_3) &&
getInt(CHANNEL_NUMBER_4) != getInt(CHANNEL_NUMBER_3);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel4()
{
if(mChannel4 == null)
{
mChannel4 = APCO25Channel.create(getInt(FREQUENCY_BAND_4), getInt(CHANNEL_NUMBER_4));
}
return mChannel4;
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
/**
* List of active TDMA data channels.
*/
public List<APCO25Channel> getActiveChannels()
{
List<APCO25Channel> channels = new ArrayList<>();
if(hasChannel1())
{
channels.add(getChannel1());
if(hasChannel2())
{
channels.add(getChannel2());
if(hasChannel3())
{
channels.add(getChannel3());
if(hasChannel4())
{
channels.add(getChannel4());
}
}
}
return channels;
}
return List.of();
}
@Override
public List<IChannelDescriptor> getChannels()
{
List<IChannelDescriptor> channels = new ArrayList<>();
if(hasChannel1())
{
channels.add(getChannel1());
if(hasChannel2())
{
channels.add(getChannel2());
if(hasChannel3())
{
channels.add(getChannel3());
if(hasChannel4())
{
channels.add(getChannel4());
}
}
}
return channels;
}
return List.of();
}
}

View File

@@ -0,0 +1,62 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
import java.util.List;
/**
* Motorola Unknown Opcode 135
*
* This message was observed on a TDMA dedicated data channel for the Victoria Radio Network (VRN). It may be an
* indicator that this is a data channel.
*/
public class MotorolaUnknownOpcode135 extends MacStructureVendor
{
/**
* Constructs the message
*
* @param message containing the message bits
* @param offset into the message for this structure
*/
public MotorolaUnknownOpcode135(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Textual representation of this message
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getOpcode());
sb.append(" MSG:").append(getSubMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,71 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import java.util.List;
/**
* Motorola TDMA Data Channel (DATCH) timeslot. Is probably used with APX-NEXT and MDT equipment.
*
* Analysis:
* - It seems to use the standard P25 Phase 2 scrambling sequence.
* - The overall signalling doesn't appear to be encrypted because the data does not appear randomized. There's a lot
* of repeated messaging with just a few bits changing.
* - It doesn't seem to be RS encoded data, or the first nibble is not protected in the RS encoding because the overall
* message doesn't change with variations in the first nibble, when there's repeating messages.
* - First 4 bits follow a pattern in repeating messages across both timeslots where the first 2 bits follows the pattern
* and the 3rd and 4th bits are set across sequences of 3 or 4 messages spanning both timeslots.
* - Not sure if the timeslots are independent of each other, or if they are strapped.
*/
public class DatchTimeslot extends Timeslot
{
/**
* Constructs a scrambled DATCH timeslot
*
* @param message containing 320 scrambled bits for the timeslot
* @param scramblingSequence to descramble the message
* @param timeslot of the message
* @param timestamp of the message
*/
public DatchTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence, int timeslot,
long timestamp)
{
super(message, DataUnitID.SCRAMBLED_DATCH, scramblingSequence, timeslot, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("TS").append(getTimeslot());
sb.append(" DATCH-SC TDMA DATA TIMESLOT MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -52,6 +52,8 @@ public class TimeslotFactory
return new FacchTimeslot(message, scramblingSequence, timeslot, timestamp);
case SCRAMBLED_SACCH:
return new SacchTimeslot(message, scramblingSequence, timeslot, timestamp);
case SCRAMBLED_DATCH:
return new DatchTimeslot(message, scramblingSequence, timeslot, timestamp);
case UNSCRAMBLED_FACCH:
return new FacchTimeslot(message, timeslot, timestamp);
case UNSCRAMBLED_SACCH:

View File

@@ -41,6 +41,7 @@ public class UnknownTimeslot extends Timeslot
StringBuilder sb = new StringBuilder();
sb.append("TS").append(getTimeslot());
sb.append(" UNKNOWN TIMESLOT - DATA UNIT ID: ").append(getDataUnitID());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}