diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index 0bd2c81b..5cc33f1d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -80,6 +80,7 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionPar import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.DecodeEventType; 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; import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket; import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket; @@ -325,7 +326,22 @@ public class DMRDecoderState extends TimeslotDecoderState .build(); broadcast(smsEvent); } - //Hytera Short Data - Radio Registration Service (RRS) + //Hytera Radio Registration Service (RRS) + else if(packet.getPacket() instanceof HyteraRrsPacket rrs) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); + + StringBuilder sb = new StringBuilder(); + sb.append("HYTERA RRS REGISTER RADIO:"); + sb.append(rrs.getDestination()); + DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp()) + .identifiers(mic) + .timeslot(getTimeslot()) + .details(sb.toString()) + .build(); + broadcast(shortDataEvent); + } + //Hytera Short Data else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp) { MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); @@ -343,7 +359,6 @@ public class DMRDecoderState extends TimeslotDecoderState sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString()); - DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp()) .identifiers(mic) .timeslot(getTimeslot()) diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java index 739b5b06..7dc60f6f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java @@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat; import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint; import io.github.dsheirer.module.decode.ip.DefinedShortDataPacket; import io.github.dsheirer.module.decode.ip.UnknownPacket; +import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket; import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket; @@ -243,8 +244,19 @@ public class PacketSequenceMessageFactory */ public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet) { - return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet, - packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + //Test to see if this is Hytera Radio Registration Service (RRS) + HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet); + + if(hyteraTokenHeader.isRRSMessage()) + { + return new DMRPacketMessage(packetSequence, new HyteraRrsPacket(hyteraTokenHeader), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } + else + { + return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet, + packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); + } } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/rrs/HyteraRrsPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/rrs/HyteraRrsPacket.java new file mode 100644 index 00000000..90801033 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/rrs/HyteraRrsPacket.java @@ -0,0 +1,130 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.hytera.rrs; + +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.hytera.sds.DestinationId; +import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraToken; +import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader; +import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenType; +import io.github.dsheirer.module.decode.p25.identifier.ipv4.DMRIpAddress; +import java.util.ArrayList; +import java.util.List; + +/** + * Hytera SDS Long Data Message with SMS encoded payload + */ +public class HyteraRrsPacket implements IPacket +{ + private HyteraTokenHeader mHeader; + private Identifier mDestination; + private List mIdentifiers; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param header to the packet within the message + */ + public HyteraRrsPacket(HyteraTokenHeader header) + { + mHeader = header; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("HYTERA RRS REGISTER"); + if(hasDestination()) + { + sb.append(" RADIO:").append(getDestination()); + } + + return sb.toString(); + } + + @Override + public HyteraTokenHeader getHeader() + { + return mHeader; + } + + /** + * Destination radio identifier for the SMS message + */ + public Identifier getDestination() + { + if(mDestination == null) + { + HyteraToken destinationToken = getHeader().getTokenByType(HyteraTokenType.ID_DESTINATION); + + if(destinationToken instanceof DestinationId destinationId) + { + if(((DestinationId) destinationToken).isIp()) + { + mDestination = DMRIpAddress.createFrom(((DestinationId) destinationToken).getId()); + } + else + { + mDestination = DMRRadio.createTo(destinationId.getId()); + } + } + } + + return mDestination; + } + + /** + * Indicates if the SMS message has a destination identifier + */ + public boolean hasDestination() + { + return getDestination() != null; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasDestination()) + { + mIdentifiers.add(getDestination()); + } + } + + return mIdentifiers; + } + + @Override + public IPacket getPayload() + { + return null; + } + + @Override + public boolean hasPayload() + { + return false; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/DestinationId.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/DestinationId.java index 6e442f73..6c84e682 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/DestinationId.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/DestinationId.java @@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.ip.hytera.sds; import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.ipv4.IPV4Address; /** * Destination Identifier @@ -45,10 +46,19 @@ public class DestinationId extends HyteraToken public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("DESTINATION:").append(getId()); + sb.append("DESTINATION:").append(getIdFormatted()); return sb.toString(); } + /** + * Indicates if this is an IP address (true) or a Radio ID (false). + * @return true if IP + */ + public boolean isIp() + { + return getId() > 0x00FFFFFF; + } + /** * Length of the content field in bytes. * @return content field length. @@ -69,6 +79,18 @@ public class DestinationId extends HyteraToken return mMessage.getInt(ID); } + public String getIdFormatted() + { + if(isIp()) + { + return new IPV4Address(getId()).toString() + " [" + (getId() & 0xFFFFFF) + "]"; + } + else + { + return String.valueOf(getId()); + } + } + @Override HyteraTokenType getTokenType() { diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenFactory.java index 7bf3f6b5..0ce6c965 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenFactory.java @@ -37,11 +37,13 @@ public class HyteraTokenFactory return switch(token) { case ID_DESTINATION -> new DestinationId(message); - case ENCODING -> new Encoding(message); case ID_MESSAGE -> new MessageId(message); - case PAYLOAD -> new Payload(message); - case MESSAGE_HEADER -> new MessageHeader(message); case ID_SOURCE -> new SourceId(message); + case ENCODING -> new Encoding(message); + case PAYLOAD -> new Payload(message); + case RRS_UNKNOWN -> new RrsUnknown(message); + case TEXT_MESSAGE_SERVICE_HEADER -> new TmsMessageHeader(message); + case RADIO_REGISTRATION_SERVICE_HEADER -> new RrsMessageHeader(message); default -> new Unknown(message); }; } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenHeader.java index 0fd0c82c..7d50ce5d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenHeader.java @@ -72,7 +72,15 @@ public class HyteraTokenHeader implements IHeader */ public boolean isSMSMessage() { - return hasTokens(HyteraTokenType.MESSAGE_HEADER, HyteraTokenType.ENCODING, HyteraTokenType.PAYLOAD); + return hasTokens(HyteraTokenType.TEXT_MESSAGE_SERVICE_HEADER, HyteraTokenType.ENCODING, HyteraTokenType.PAYLOAD); + } + + /** + * Indicates if this header has RRS service message. + */ + public boolean isRRSMessage() + { + return hasTokens(HyteraTokenType.RADIO_REGISTRATION_SERVICE_HEADER, HyteraTokenType.ID_DESTINATION); } /** @@ -153,7 +161,7 @@ public class HyteraTokenHeader implements IHeader length += contentLength * 8; //Actual content } - HyteraToken field = HyteraTokenFactory.getField(token, mMessage.getSubMessage(offset, offset + length + 1)); + HyteraToken field = HyteraTokenFactory.getField(token, mMessage.getSubMessage(offset, offset + length)); offset += length; mParsedTokens.add(field); } diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenType.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenType.java index 03c34f9e..effd8a28 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenType.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/HyteraTokenType.java @@ -29,13 +29,15 @@ package io.github.dsheirer.module.decode.ip.hytera.sds; */ public enum HyteraTokenType { - //Note: this may be the message type, but is definitely followed by 4-byte total length field. Need more examples - MESSAGE_HEADER(0x0201, 4), //First 2 bytes are total message length, 2nd 2 bytes = unknown + RADIO_REGISTRATION_SERVICE_HEADER(0x0200, 1), //Total message length + TEXT_MESSAGE_SERVICE_HEADER(0x0201, 4), //First 2 bytes are total message length, 2nd 2 bytes = unknown + ID_MESSAGE(0x0001, -1), ID_SOURCE(0x0002, -1), ID_DESTINATION(0x0003, -1), ENCODING(0x0101, -1), PAYLOAD(0x0102, -1), + RRS_UNKNOWN(0x0703, 0), UNKNOWN(-1, -1); private int mOpcode; diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsMessageHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsMessageHeader.java new file mode 100644 index 00000000..dfbe8ff2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsMessageHeader.java @@ -0,0 +1,66 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.hytera.sds; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Radio Registration Service (RRS) Message Header + */ +public class RrsMessageHeader extends HyteraToken +{ + private static int[] MESSAGE_LENGTH_BCD_TENS = new int[]{16, 17, 18, 19}; + private static int[] MESSAGE_LENGTH_BCD_ONES = new int[]{20, 21, 22, 23}; + + /** + * Constructs an instance + * + * @param message containing both the token, run-length, and content + */ + public RrsMessageHeader(CorrectedBinaryMessage message) + { + super(message); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("RRS HEADER LENGTH:").append(getMessageLength()); + return sb.toString(); + } + + @Override + HyteraTokenType getTokenType() + { + return HyteraTokenType.RADIO_REGISTRATION_SERVICE_HEADER; + } + + /** + * Overall message length in bytes + * @return message length. + */ + public int getMessageLength() + { + int length = mMessage.getInt(MESSAGE_LENGTH_BCD_TENS) * 10; + length += mMessage.getInt(MESSAGE_LENGTH_BCD_ONES); + return length; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsUnknown.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsUnknown.java new file mode 100644 index 00000000..da568240 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/RrsUnknown.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.ip.hytera.sds; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * Unknown RRS field parsed from a Hytera Radio Registration Service (RRS) message + */ +public class RrsUnknown extends HyteraToken +{ + /** + * Constructs an instance + * + * @param message containing both the token, run-length, and content + */ + public RrsUnknown(CorrectedBinaryMessage message) + { + super(message); + } + + @Override + public String toString() + { + return "RRS UNKNOWN TOKEN"; + } + + @Override + HyteraTokenType getTokenType() + { + return HyteraTokenType.RRS_UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/MessageHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/TmsMessageHeader.java similarity index 73% rename from src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/MessageHeader.java rename to src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/TmsMessageHeader.java index 3601145c..56fd814b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/MessageHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/hytera/sds/TmsMessageHeader.java @@ -22,16 +22,19 @@ package io.github.dsheirer.module.decode.ip.hytera.sds; import io.github.dsheirer.bits.CorrectedBinaryMessage; /** - * Message Header - * + * Text Message Service (TMS) Message Header + *

* Initial token that specifies the overall message length and some other unidentified bit fields. - * + *

* Note: I've only confirmed this is the start token for long SMS messages, but it may also be the start token for * other packet data messages like GPS or any other packet overlay application. */ -public class MessageHeader extends HyteraToken +public class TmsMessageHeader extends HyteraToken { - private static int[] MESSAGE_LENGTH = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + private static int[] MESSAGE_LENGTH_BCD_THOUSANDS = new int[]{16, 17, 18, 19}; + private static int[] MESSAGE_LENGTH_BCD_HUNDREDS = new int[]{20, 21, 22, 23}; + private static int[] MESSAGE_LENGTH_BCD_TENS = new int[]{24, 25, 26, 27}; + private static int[] MESSAGE_LENGTH_BCD_ONES = new int[]{28, 29, 30, 31}; private static int[] MESSAGE_FLAGS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; /** @@ -39,7 +42,7 @@ public class MessageHeader extends HyteraToken * * @param message containing both the token, run-length, and content */ - public MessageHeader(CorrectedBinaryMessage message) + public TmsMessageHeader(CorrectedBinaryMessage message) { super(message); } @@ -56,7 +59,7 @@ public class MessageHeader extends HyteraToken @Override HyteraTokenType getTokenType() { - return HyteraTokenType.MESSAGE_HEADER; + return HyteraTokenType.TEXT_MESSAGE_SERVICE_HEADER; } /** @@ -65,6 +68,7 @@ public class MessageHeader extends HyteraToken * - Priority/Flash (boolean) * - Encryption Type (enum of at least 4 values, ie 2+ bits) * - Emergency (boolean) + * * @return */ public String getFlags() @@ -78,6 +82,10 @@ public class MessageHeader extends HyteraToken */ public int getMessageLength() { - return mMessage.getInt(MESSAGE_LENGTH); + int length = mMessage.getInt(MESSAGE_LENGTH_BCD_THOUSANDS) * 1000; + length += mMessage.getInt(MESSAGE_LENGTH_BCD_HUNDREDS) * 100; + length += mMessage.getInt(MESSAGE_LENGTH_BCD_TENS) * 10; + length += mMessage.getInt(MESSAGE_LENGTH_BCD_ONES); + return length; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/ipv4/DMRIpAddress.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/ipv4/DMRIpAddress.java new file mode 100644 index 00000000..5f9ba72a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/ipv4/DMRIpAddress.java @@ -0,0 +1,51 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.identifier.ipv4; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.ipv4.IPV4Address; +import io.github.dsheirer.identifier.ipv4.IPV4Identifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * DMR IPV4 Address + */ +public class DMRIpAddress extends IPV4Identifier +{ + public DMRIpAddress(IPV4Address ipAddress, Role role) + { + super(ipAddress, role); + } + + public Protocol getProtocol() + { + return Protocol.DMR; + } + + public static DMRIpAddress createTo(int value) + { + return new DMRIpAddress(new IPV4Address(value), Role.TO); + } + + public static DMRIpAddress createFrom(int value) + { + return new DMRIpAddress(new IPV4Address(value), Role.FROM); + } +}