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 2f6db237..d7a59d5d 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 @@ -101,6 +101,7 @@ import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacke import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket; import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket; import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket; +import io.github.dsheirer.module.decode.ip.mototrbo.tms.TMSPacket; import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket; import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest; @@ -503,6 +504,19 @@ public class DMRDecoderState extends TimeslotDecoderState .build(); broadcast(shortDataEvent); } + //Motorola TMS + else if(packet.getPacket() instanceof TMSPacket tms) + { + MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers()); + + DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.TEXT_MESSAGE, packet.getTimestamp()) + .channel(getCurrentChannel()) + .identifiers(mic) + .timeslot(getTimeslot()) + .details(tms.toString()) + .build(); + broadcast(shortDataEvent); + } //Motorola XCMP else if(packet.getPacket() instanceof XCMPPacket xcmp) { diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java index 9672b750..0ab74ef9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -78,6 +78,7 @@ public enum DecodeEventType SMS("SMS"), STATION_ID("Station ID"), STATUS("Status"), + TEXT_MESSAGE("Text Message"), UDP_PACKET("UDP/IP Packet"), UNKNOWN_PACKET("Unknown Packet"), XCMP("Motorola XCMP"), diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java index a631dc23..a40dc388 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/ip/PacketMessageFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header; import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet; import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket; import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket; +import io.github.dsheirer.module.decode.ip.mototrbo.tms.TMSPacket; import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket; import io.github.dsheirer.module.decode.ip.udp.UDPPacket; import io.github.dsheirer.module.decode.ip.udp.UDPPort; @@ -132,7 +133,7 @@ public class PacketMessageFactory case 4005: //Automatic Registration Service return new ARSPacket(binaryMessage, offset); case 4007: //Text Message Service - break; + return new TMSPacket(binaryMessage, offset); case 4008: //Telemetry Service break; case 4009: //Over The Air Programming (OTAP) diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSHeader.java b/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSHeader.java new file mode 100644 index 00000000..3e882ca5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSHeader.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 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.mototrbo.tms; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.Header; +import java.util.Collections; +import java.util.List; + +/** + * Text Messaging Service (TMS) Header + * + * The payload contains 4x bytes of ?UTF-8? values (0-9) indicating the message length followed by UTF-16 Little Endian + * characters. + */ +public class TMSHeader extends Header +{ + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the header + * @param offset to the header within the message + */ + public TMSHeader(BinaryMessage message, int offset) + { + super(message, offset); + } + + /** + * Character count is stored in the first 4x bytes as UTF-8 values. + */ + public int getCharacterCount() + { + StringBuilder sb = new StringBuilder(); + sb.append((char)getMessage().getByte(getOffset() + 0)); + sb.append((char)getMessage().getByte(getOffset() + 8)); + sb.append((char)getMessage().getByte(getOffset() + 16)); + sb.append((char)getMessage().getByte(getOffset() + 24)); + + try + { + return Integer.parseInt(sb.toString()); + } + catch(Exception e) + { + return 0; + } + } + + /** + * Length of this header in bits + */ + @Override + public int getLength() + { + return 32; + } + + /** + * Identifiers for this packet + */ + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSPacket.java b/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSPacket.java new file mode 100644 index 00000000..fbf2ec24 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/ip/mototrbo/tms/TMSPacket.java @@ -0,0 +1,102 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 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.mototrbo.tms; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.ip.IPacket; +import io.github.dsheirer.module.decode.ip.Packet; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +/** + * Text Messaging Service (TMS) message packet. + */ +public class TMSPacket extends Packet +{ + private TMSHeader mHeader; + + /** + * Constructs a parser for a header contained within a binary message starting at the offset. + * + * @param message containing the packet + * @param offset to the packet within the message + */ + public TMSPacket(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("TEXT MESSAGE: ").append(getTextMessage()); + return sb.toString(); + } + + /** + * Extracts the text message payload. + */ + public String getTextMessage() + { + int byteCount = (getMessage().length() - 32) / 8; + + if(byteCount > 0) + { + byte[] message = new byte[byteCount]; + + for(int x = 0; x < byteCount; x++) + { + message[x] = getMessage().getByte(getOffset() + 32 + (x * 8)); + } + + return new String(message, StandardCharsets.UTF_16LE); + } + + return "(ERROR-INSUFFICIENT BYTES)"; + } + + + @Override + public TMSHeader getHeader() + { + if(mHeader == null) + { + mHeader = new TMSHeader(getMessage(), getOffset()); + } + + return mHeader; + } + + @Override + public IPacket getPayload() + { + //There is no child payload. + return null; + } + + @Override + public List getIdentifiers() + { + //Massage and Header don't have any additional identifiers. + return Collections.emptyList(); + } +}