From 5c9c3fa5034a8fb081a8de837ab322f7513a94cf Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Tue, 12 Nov 2024 02:22:22 -0500 Subject: [PATCH] #2089 Motorola P25P2 TDMA data channel support (#2090) Co-authored-by: Dennis Sheirer --- .../dsheirer/gui/viewer/P25P2Viewer.java | 21 ++ .../p25/P25TrafficChannelEventTracker.java | 9 + .../decode/p25/P25TrafficChannelManager.java | 94 ++++++ .../decode/p25/phase1/P25P1DecoderState.java | 18 ++ .../P25P1NetworkConfigurationMonitor.java | 23 +- .../p25/phase1/message/tsbk/Opcode.java | 6 +- .../message/tsbk/TSBKMessageFactory.java | 4 + ...laExplicitTDMADataChannelAnnouncement.java | 143 ++++++++++ .../decode/p25/phase2/P25P2DecoderState.java | 8 + .../p25/phase2/P25P2MessageProcessor.java | 7 +- .../p25/phase2/enumeration/DataUnitID.java | 5 +- .../phase2/message/mac/MacMessageFactory.java | 6 + .../p25/phase2/message/mac/MacOpcode.java | 2 + .../structure/MacStructureVariableLength.java | 8 + .../motorola/MotorolaTDMADataChannel.java | 269 ++++++++++++++++++ .../motorola/MotorolaUnknownOpcode135.java | 62 ++++ .../p25/phase2/timeslot/DatchTimeslot.java | 71 +++++ .../p25/phase2/timeslot/TimeslotFactory.java | 2 + .../p25/phase2/timeslot/UnknownTimeslot.java | 1 + 19 files changed, 751 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/motorola/osp/MotorolaExplicitTDMADataChannelAnnouncement.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTDMADataChannel.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaUnknownOpcode135.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/DatchTimeslot.java diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java index 4bf91761b..4166ad43c 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java @@ -90,6 +90,9 @@ public class P25P2Viewer extends VBox private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.class); private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p2"; + private static final String LAST_WACN_VALUE = "last.wacn.value.p25p2"; + private static final String LAST_SYSTEM_VALUE = "last.system.value.p25p2"; + private static final String LAST_NAC_VALUE = "last.nac.value.p25p2"; private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*"; private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.class); private Button mSelectFileButton; @@ -415,6 +418,12 @@ private IntegerTextField getWACNTextField() if(mWACNTextField == null) { mWACNTextField = new IntegerTextField(); + mWACNTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_WACN_VALUE, getWACNTextField().get())); + int previous = mPreferences.getInt(LAST_WACN_VALUE, 0); + if(previous > 0) + { + getWACNTextField().set(previous); + } } return mWACNTextField; @@ -425,6 +434,12 @@ private IntegerTextField getSystemTextField() if(mSystemTextField == null) { mSystemTextField = new IntegerTextField(); + mSystemTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_SYSTEM_VALUE, getSystemTextField().get())); + int previous = mPreferences.getInt(LAST_SYSTEM_VALUE, 0); + if(previous > 0) + { + getSystemTextField().set(previous); + } } return mSystemTextField; @@ -435,6 +450,12 @@ private IntegerTextField getNACTextField() if(mNACTextField == null) { mNACTextField = new IntegerTextField(); + mNACTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_NAC_VALUE, getNACTextField().get())); + int previous = mPreferences.getInt(LAST_NAC_VALUE, 0); + if(previous > 0) + { + getNACTextField().set(previous); + } } return mNACTextField; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java index beedd105d..2999d5497 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java @@ -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 boolean isStale(long timestamp) 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 diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 1c5ae213a..c8ad3b686 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -58,6 +58,7 @@ 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 void processP2TrafficCurrentUser(long frequency, int timeslot, Identifier } } + /** + * 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. * diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java index e42f4506a..ffa49414d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java @@ -118,6 +118,7 @@ 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 @@ private void processTSBK(P25P1Message message) 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 @@ private void processTSBK(P25P1Message message) } } + /** + * 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 */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1NetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1NetworkConfigurationMonitor.java index 8e0254558..19cd0cea5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1NetworkConfigurationMonitor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1NetworkConfigurationMonitor.java @@ -21,6 +21,7 @@ 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.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 mSecondaryControlChannels = new TreeMap<>(); - //Current Site Data Channel + //Current Site Data Channel(s) private SNDCPDataChannelAnnouncementExplicit mSNDCPDataChannel; + private Map mTDMADataChannelMap = new HashMap<>(); //Current Site Services private SystemServiceBroadcast mTSBKSystemServiceBroadcast; @@ -178,6 +181,12 @@ public void process(TSBKMessage tsbk) 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 @@ else if(mAMBTCRFSSStatusBroadcast != null) 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 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"); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/Opcode.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/Opcode.java index b8335e640..0071081be 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/Opcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/Opcode.java @@ -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 static Opcode fromValue(int value, Direction direction, Vendor vendor) 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; } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java index 03f3f0528..c64e2a4ba 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java @@ -35,6 +35,7 @@ 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 static TSBKMessage create(Direction direction, P25P1DataUnitID dataUnitID 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; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/motorola/osp/MotorolaExplicitTDMADataChannelAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/motorola/osp/MotorolaExplicitTDMADataChannelAnnouncement.java new file mode 100644 index 000000000..efa15d537 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/motorola/osp/MotorolaExplicitTDMADataChannelAnnouncement.java @@ -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 + * **************************************************************************** + */ + +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 getIdentifiers() + { + return Collections.emptyList(); + } + + @Override + public List getChannels() + { + if(hasChannel()) + { + return List.of(getChannel()); + } + + return List.of(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java index de61291af..7830a50cb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java @@ -126,6 +126,7 @@ 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 @@ else if(message instanceof AbstractVoiceTimeslot) //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 @@ private void processMacMessage(MacMessage message) 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; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java index 9a684bc7f..3a4f60536 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java @@ -36,6 +36,7 @@ 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 void receive(IMessage message) 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; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java index fa56f324a..c804e0f86 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java @@ -25,7 +25,6 @@ */ 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 static DataUnitID fromEncodedValue(int value) case 0x9A: return SCRAMBLED_FACCH; case 0xA3: - return RESERVED_A; + return SCRAMBLED_DATCH; case 0xB4: return RESERVED_B; case 0xC6: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java index 646bebb25..c14dc1a97 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java @@ -143,8 +143,10 @@ 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 static MacStructure createMacStructure(CorrectedBinaryMessage message, in 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: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java index 1fcc1d809..5271e4e72 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java @@ -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. diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/MacStructureVariableLength.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/MacStructureVariableLength.java index e15c65dce..8b1b05e82 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/MacStructureVariableLength.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/MacStructureVariableLength.java @@ -70,4 +70,12 @@ public int getLength() { return getLength(getMessage(), getOffset()); } + + /** + * Message fragment for this variable length message. + */ + public CorrectedBinaryMessage getSubMessage() + { + return getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8)); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTDMADataChannel.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTDMADataChannel.java new file mode 100644 index 000000000..d03767c19 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTDMADataChannel.java @@ -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 + * **************************************************************************** + */ + +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 getIdentifiers() + { + return List.of(); + } + + /** + * List of active TDMA data channels. + */ + public List getActiveChannels() + { + List 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 getChannels() + { + List 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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaUnknownOpcode135.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaUnknownOpcode135.java new file mode 100644 index 000000000..ce72abcff --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaUnknownOpcode135.java @@ -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 + * **************************************************************************** + */ + +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 getIdentifiers() + { + return List.of(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/DatchTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/DatchTimeslot.java new file mode 100644 index 000000000..58733f572 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/DatchTimeslot.java @@ -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 + * **************************************************************************** + */ + +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 getIdentifiers() + { + return List.of(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java index 0a72226ba..45fa20f25 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/TimeslotFactory.java @@ -52,6 +52,8 @@ public static Timeslot getTimeslot(CorrectedBinaryMessage message, BinaryMessage 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: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/UnknownTimeslot.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/UnknownTimeslot.java index 1f89b935a..c545ed872 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/UnknownTimeslot.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/UnknownTimeslot.java @@ -41,6 +41,7 @@ public String toString() 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(); }