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 d8547cb4..bd4d8984 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 @@ -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 @@ -64,6 +64,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS; @@ -326,6 +327,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList { processTalkerAlias(talkerAlias); } + else if(iMessage instanceof LinkControlWord lcw) + { + processLC(lcw, iMessage.getTimestamp(), false); + } } /** @@ -2093,6 +2098,26 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList broadcast(event); } break; + case L3HARRIS_TALKER_GPS_COMPLETE: + if(lcw instanceof LCHarrisTalkerGPSComplete gps) + { + mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), gps.getLocation(), timestamp, lcw.toString()); + + //We want to preserve the current TO/FROM and any other identifiers for the GPS event and add the GPS location + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.update(gps.getIdentifiers()); + + PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp) + .location(gps.getGeoPosition()) + .channel(getCurrentChannel()) + .details(gps.getLocation().toString()) + .end(timestamp) + .protocol(Protocol.APCO25) + .identifiers(mic) + .build(); + broadcast(event); + } + break; default: // if(lcw.getVendor().isLoggable()) // { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java index 759f6f20..ff834587 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.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 @@ -29,6 +29,9 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.IExtendedSourceMes import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.HarrisTalkerAliasAssembler; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBase; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock1; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock2; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkerAliasAssembler; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message; @@ -68,6 +71,8 @@ public class P25P1MessageProcessor implements Listener private HDUMessage mHeldHDUMessage; private LDU1Message mHeldLDU1Message; private LDU2Message mHeldLDU2Message; + private LCHarrisTalkerGPSBlock1 mHeldHarrisGPSBlock1; + private long mHeldHarrisGPSBlock1Timestamp; /** * Motorola talker alias assembler for link control header and data blocks. @@ -139,6 +144,26 @@ public class P25P1MessageProcessor implements Listener processSourceIDExtension(null); dispatch(ldu1); + + //Process Harris Talker GPS messages + if(lcw instanceof LCHarrisTalkerGPSBlock1 block1) + { + mHeldHarrisGPSBlock1 = block1; + mHeldHarrisGPSBlock1Timestamp = ldu1.getTimestamp(); + } + else if(lcw instanceof LCHarrisTalkerGPSBlock2 block2 && mHeldHarrisGPSBlock1 != null) + { + //Ensure block 1 and block 2 are within 2 seconds of each other + if(Math.abs(ldu1.getTimestamp() - mHeldHarrisGPSBlock1Timestamp) < 2000) + { + LCHarrisTalkerGPSComplete gps = LCHarrisTalkerGPSComplete.create(mHeldHarrisGPSBlock1, block2, + ldu1.getTimestamp()); + dispatch(gps); + } + + mHeldHarrisGPSBlock1 = null; + mHeldHarrisGPSBlock1Timestamp = 0; + } } else if(message instanceof LDU2Message ldu2) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java index 189b3e83..f980a6c8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.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 @@ -106,8 +106,9 @@ public enum LinkControlOpcode MOTOROLA_UNKNOWN("MOTOROLA UNKNOWN", -1), L3HARRIS_RETURN_TO_CONTROL_CHANNEL("UNKNOWN OPCODE 10", 10), - L3HARRIS_UNKNOWN_2A("UNKNOWN OPCODE 42", 42), - L3HARRIS_UNKNOWN_2B("UNKNOWN OPCODE 43", 43), + L3HARRIS_TALKER_GPS_BLOCK1("TALKER GPS 1/2", 42), + L3HARRIS_TALKER_GPS_BLOCK2("TALKER GPS 2/2", 43), + L3HARRIS_TALKER_GPS_COMPLETE("TALKER GPS", -2), L3HARRIS_TALKER_ALIAS_BLOCK_1("TALKER ALIAS BLOCK 1", 50), L3HARRIS_TALKER_ALIAS_BLOCK_2("TALKER ALIAS BLOCK 2", 51), L3HARRIS_TALKER_ALIAS_BLOCK_3("TALKER ALIAS BLOCK 3", 52), @@ -151,8 +152,7 @@ public enum LinkControlOpcode /** * L3Harris Opcodes */ - public static final EnumSet L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_RETURN_TO_CONTROL_CHANNEL, - L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_TALKER_ALIAS_BLOCK_1, L3HARRIS_TALKER_ALIAS_BLOCK_2, + public static final EnumSet L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_RETURN_TO_CONTROL_CHANNEL, L3HARRIS_TALKER_GPS_BLOCK1, L3HARRIS_TALKER_GPS_BLOCK2, L3HARRIS_TALKER_ALIAS_BLOCK_1, L3HARRIS_TALKER_ALIAS_BLOCK_2, L3HARRIS_TALKER_ALIAS_BLOCK_3, L3HARRIS_TALKER_ALIAS_BLOCK_4, L3HARRIS_UNKNOWN); /** @@ -221,9 +221,9 @@ public enum LinkControlOpcode case 10: return L3HARRIS_RETURN_TO_CONTROL_CHANNEL; case 42: - return L3HARRIS_UNKNOWN_2A; + return L3HARRIS_TALKER_GPS_BLOCK1; case 43: - return L3HARRIS_UNKNOWN_2B; + return L3HARRIS_TALKER_GPS_BLOCK2; case 50: return L3HARRIS_TALKER_ALIAS_BLOCK_1; case 51: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java index 0b553879..41d14d1b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.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 @@ -25,8 +25,8 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisT import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock2; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock3; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock4; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode42; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode43; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock1; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock2; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaFailsoft; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupGroupDelete; @@ -161,10 +161,10 @@ public class LinkControlWordFactory case L3HARRIS_RETURN_TO_CONTROL_CHANNEL: return new LCHarrisReturnToControlChannel(message); - case L3HARRIS_UNKNOWN_2A: - return new LCHarrisUnknownOpcode42(message); - case L3HARRIS_UNKNOWN_2B: - return new LCHarrisUnknownOpcode43(message); + case L3HARRIS_TALKER_GPS_BLOCK1: + return new LCHarrisTalkerGPSBlock1(message); + case L3HARRIS_TALKER_GPS_BLOCK2: + return new LCHarrisTalkerGPSBlock2(message); case L3HARRIS_TALKER_ALIAS_BLOCK_1: return new LCHarrisTalkerAliasBlock1(message); case L3HARRIS_TALKER_ALIAS_BLOCK_2: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode43.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock1.java similarity index 80% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode43.java rename to src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock1.java index d9edcdff..9342039e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode43.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock1.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 @@ -26,19 +26,16 @@ import java.util.Collections; import java.util.List; /** - * L3Harris Unknown Opcode 43 (0x2B) - *

- * Observed in a voice traffic channel carried by an LDU1. This was transmitted once following an Opcode 42 (0x2A) - * which is also unknown. + * L3Harris Talker GPS - Block 1/2 (Opcode:0x2A) */ -public class LCHarrisUnknownOpcode43 extends LinkControlWord +public class LCHarrisTalkerGPSBlock1 extends LinkControlWord { /** * Constructs a Link Control Word from the binary message sequence. * * @param message */ - public LCHarrisUnknownOpcode43(CorrectedBinaryMessage message) + public LCHarrisTalkerGPSBlock1(CorrectedBinaryMessage message) { super(message); } @@ -58,7 +55,7 @@ public class LCHarrisUnknownOpcode43 extends LinkControlWord } else { - sb.append("L3HARRIS UNKNOWN OPCODE 43 MSG:").append(getMessage().toHexString()); + sb.append("L3HARRIS TALKER GPS BLOCK 1/2 MSG:").append(getMessage().toHexString()); } return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode42.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock2.java similarity index 80% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode42.java rename to src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock2.java index 2bb4bb9b..e00422ac 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisUnknownOpcode42.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSBlock2.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 @@ -26,19 +26,16 @@ import java.util.Collections; import java.util.List; /** - * L3Harris Unknown Opcode 42 (0x2A) - *

- * Observed in a voice traffic channel carried by an LDU1. This was transmitted once followed by an Opcode 43 (0x2B) - * which is also unknown. + * L3Harris Talker GPS - Block 2/2 (Opcode:0x2B) */ -public class LCHarrisUnknownOpcode42 extends LinkControlWord +public class LCHarrisTalkerGPSBlock2 extends LinkControlWord { /** * Constructs a Link Control Word from the binary message sequence. * * @param message */ - public LCHarrisUnknownOpcode42(CorrectedBinaryMessage message) + public LCHarrisTalkerGPSBlock2(CorrectedBinaryMessage message) { super(message); } @@ -58,7 +55,7 @@ public class LCHarrisUnknownOpcode42 extends LinkControlWord } else { - sb.append("L3HARRIS UNKNOWN OPCODE 42 MSG:").append(getMessage().toHexString()); + sb.append("L3HARRIS TALKER GPS BLOCK 2/2 MSG:").append(getMessage().toHexString()); } return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSComplete.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSComplete.java new file mode 100644 index 00000000..10a80d10 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerGPSComplete.java @@ -0,0 +1,190 @@ +/* + * ***************************************************************************** + * 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.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.identifier.P25Location; +import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlOpcode; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisGPS; +import io.github.dsheirer.module.decode.p25.reference.Vendor; +import io.github.dsheirer.protocol.Protocol; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; +import org.jdesktop.swingx.mapviewer.GeoPosition; + +/** + * L3Harris Talker GPS Complete. This message is assembled from FLC L3Harris Talker GPS Block 1 and Block 2. + */ +public class LCHarrisTalkerGPSComplete extends LinkControlWord implements IMessage +{ + private static final DecimalFormat GPS_FORMAT = new DecimalFormat("0.000000"); + private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss"); + static { + SDF.setTimeZone(TimeZone.getTimeZone("UTC")); + } + private P25Location mLocation; + private GeoPosition mGeoPosition; + private List mIdentifiers; + private long mTimestamp; + + /** + * Constructs a Link Control Word from the binary message sequence. + * @param message constructed from block 1 and block 2 fragments + */ + private LCHarrisTalkerGPSComplete(CorrectedBinaryMessage message, long timestamp) + { + super(message); + mTimestamp = timestamp; + } + + @Override + public Protocol getProtocol() + { + return Protocol.APCO25; + } + + @Override + public long getTimestamp() + { + return mTimestamp; + } + + @Override + public int getTimeslot() + { + return P25P1Message.TIMESLOT_0; + } + + /** + * Constructs an instance of this message from block 1 and block 2 FLC GPS message fragments. + * @param block1 FLC Harris Talker GPS + * @param block2 FLC Harris Talker GPS + * @param timestamp for the message + * @return instance of this message. + */ + public static LCHarrisTalkerGPSComplete create(LCHarrisTalkerGPSBlock1 block1, LCHarrisTalkerGPSBlock2 block2, + long timestamp) + { + CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(112); + cbm.load(0, block1.getMessage().getSubMessage(16, 72)); + cbm.load(56, block2.getMessage().getSubMessage(16, 72)); + return new LCHarrisTalkerGPSComplete(cbm, timestamp); + } + + /** + * Textual representation of this message + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("L3HARRIS TALKER GPS "); + GeoPosition geo = getGeoPosition(); + sb.append(GPS_FORMAT.format(geo.getLatitude())).append(" ").append(GPS_FORMAT.format(geo.getLongitude())); + sb.append(" HEADING:").append(getHeading()); + sb.append(" TIME:").append(SDF.format(getTimestampMs())); + sb.append(" UTC MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + @Override + public Vendor getVendor() + { + return Vendor.HARRIS; + } + + @Override + public LinkControlOpcode getOpcode() + { + return LinkControlOpcode.L3HARRIS_TALKER_GPS_COMPLETE; + } + + @Override + public int getOpcodeNumber() + { + return getOpcode().getCode(); + } + + /** + * Heading + * @return heading, 0-359 degrees. + */ + public int getHeading() + { + return L3HarrisGPS.parseHeading(getMessage(), 0); + } + + /** + * GPS Position time in milliseconds. + * @return time in ms UTC + */ + public long getTimestampMs() + { + return L3HarrisGPS.parseTimestamp(getMessage(), 0); + } + + /** + * GPS Location + * @return location in decimal degrees + */ + public P25Location getLocation() + { + if(mLocation == null) + { + GeoPosition geoPosition = getGeoPosition(); + mLocation = P25Location.createFrom(geoPosition.getLatitude(), geoPosition.getLongitude()); + } + + return mLocation; + } + + /** + * Geo position + * @return position + */ + public GeoPosition getGeoPosition() + { + if(mGeoPosition == null) + { + mGeoPosition = new GeoPosition(L3HarrisGPS.parseLatitude(getMessage(), 0), + L3HarrisGPS.parseLongitude(getMessage(), 0)); + } + + return mGeoPosition; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getLocation()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGPS.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGPS.java new file mode 100644 index 00000000..d44050b8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGPS.java @@ -0,0 +1,128 @@ +/* + * ***************************************************************************** + * 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.p25.phase2.message.mac.structure.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.bits.IntField; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Utility class to parse L3Harris GPS data from P25 Phase 1 and Phase 2 messages. + */ +public class L3HarrisGPS +{ + public static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss"); + static { + SDF.setTimeZone(TimeZone.getTimeZone("UTC")); + } + private static final DecimalFormat GPS_FORMAT = new DecimalFormat("0.000000"); + + //Bits 0 & 1 not set in sample data - seems unused for a 1/10000th of a minute number system + private static final IntField LAT_MINUTES_FRACTIONAL = IntField.range(0, 15); + private static final int LAT_HEMISPHERE = 16; + //Bit 39 & 41 not set in sample data + private static final IntField LAT_MINUTES = IntField.range(17, 23); + private static final IntField LAT_DEGREES = IntField.range(24, 31); + + //Bits 56 & 57 not set in sample data - seems unused for a 1/5000th of a minute number system + private static final IntField LONG_MINUTES_FRACTIONAL = IntField.range(32, 47); + private static final int LONG_HEMISPHERE = 48; + //Bit 71 & 73 not set in sample data + private static final IntField LONG_MINUTES = IntField.range(49, 55); + private static final IntField LONG_DEGREES = IntField.range(56, 63); + + //There's a leading bit missing from GPS Time to get from (2^16) to (2^17) needed address space (86,400 total seconds) + private static final IntField GPS_TIME = IntField.range(64, 79); + private static final int GPS_TIME_MSB = 80; + + //This may not be accurate. + private static final IntField HEADING = IntField.range(95, 103); + + /** + * Parses the latitude value from the message + * @param message containing latitude value. + * @param offset into the message to the start of the formatted GPS data. + * @return latitude in degrees decimal. + */ + public static double parseLatitude(CorrectedBinaryMessage message, int offset) + { + return parseCoordinate(message, offset, LAT_DEGREES, LAT_MINUTES, LAT_MINUTES_FRACTIONAL, LAT_HEMISPHERE); + } + + /** + * Parses the longitude value from the message + * @param message containing longitude value. + * @param offset into the message to the start of the formatted GPS data. + * @return longitude in degrees decimal. + */ + public static double parseLongitude(CorrectedBinaryMessage message, int offset) + { + return parseCoordinate(message, offset, LONG_DEGREES, LONG_MINUTES, LONG_MINUTES_FRACTIONAL, LONG_HEMISPHERE); + } + + /** + * Parses the latitude or longitude coordinate value from the message + * @param message containing the coordinate + * @param offset into the message to the start of the GPS data + * @param degrees field definition + * @param minutes field definition + * @param fractional field definition + * @param hemisphere field definition + * @return value in degrees decimal. + */ + private static double parseCoordinate(CorrectedBinaryMessage message, int offset, IntField degrees, + IntField minutes, IntField fractional, int hemisphere) + { + double longitude = message.getInt(degrees, offset); + longitude += ((message.getInt(minutes, offset) + (message.getInt(fractional, offset) / 10000.0)) / 60.0); + longitude *= (message.get(hemisphere + offset) ? -1 : 1); + return longitude; + } + + /** + * Parse the timestamp + * @param message containing the timestamp + * @param offset into the message to the start of the GPS data. + * @return timestamp in milliseconds + */ + public static long parseTimestamp(CorrectedBinaryMessage message, int offset) + { + long seconds = message.getInt(GPS_TIME, offset); + + if(message.get(offset + GPS_TIME_MSB)) + { + seconds *= 2; + } + + return seconds * 1000; //Convert seconds to milliseconds. + } + + /** + * Heading + * + * @return heading, 0-359 degrees. + */ + public static int parseHeading(CorrectedBinaryMessage message, int offset) + { + return message.getInt(HEADING, offset); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerGpsLocation.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerGpsLocation.java index 6d18fbec..6d189bfe 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerGpsLocation.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerGpsLocation.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 @@ -20,7 +20,6 @@ package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris; import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.bits.IntField; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.module.decode.dmr.identifier.P25Location; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor; @@ -39,37 +38,12 @@ import org.jdesktop.swingx.mapviewer.GeoPosition; */ public class L3HarrisTalkerGpsLocation extends MacStructureVendor { - public static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss"); + private static final DecimalFormat GPS_FORMAT = new DecimalFormat("0.000000"); + private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss"); static { SDF.setTimeZone(TimeZone.getTimeZone("UTC")); } - private static final DecimalFormat GPS_FORMAT = new DecimalFormat("0.000000"); - private static final DecimalFormat FIXED = new DecimalFormat("000"); - - //Bits 24 & 25 not set in sample data - seems unused for a 1/5000th of a minute number system - private static final IntField LATITUDE_MINUTES_FRACTIONAL = IntField.range(24, 38); - private static final int LATITUDE_HEMISPHERE = 40; - //Bit 39 & 41 not set in sample data - private static final IntField LATITUDE_MINUTES = IntField.range(42, 47); - private static final IntField LATITUDE_DEGREES = IntField.range(48, 55); - - //Bits 56 & 57 not set in sample data - seems unused for a 1/5000th of a minute number system - private static final IntField LONGITUDE_MINUTES_FRACTIONAL = IntField.range(56, 70); - private static final int LONGITUDE_HEMISPHERE = 72; - //Bit 71 & 73 not set in sample data - private static final IntField LONGITUDE_MINUTES = IntField.range(74, 79); - private static final IntField LONGITUDE_DEGREES = IntField.range(80, 87); - - //There's a leading bit missing from GPS Time to get from (2^16) to (2^17) needed address space (86,400 total seconds) - private static final IntField GPS_TIME = IntField.range(88, 103); - - private static final IntField U1 = IntField.length8(OCTET_14_BIT_104); - private static final IntField U2 = IntField.length8(OCTET_15_BIT_112); - private static final IntField U3 = IntField.length8(OCTET_16_BIT_120); - private static final IntField U4 = IntField.length8(OCTET_17_BIT_128); - - //This may not be accurate. - private static final IntField HEADING = IntField.range(119, 127); + private static final int DATA_OFFSET = 24; private P25Location mLocation; private GeoPosition mGeoPosition; @@ -102,10 +76,11 @@ public class L3HarrisTalkerGpsLocation extends MacStructureVendor sb.append("VENDOR:").append(getVendor()).append(" TALKER GPS "); } - sb.append(GPS_FORMAT.format(getLatitude())).append(" ").append(GPS_FORMAT.format(getLongitude())); + GeoPosition geo = getGeoPosition(); + sb.append(GPS_FORMAT.format(geo.getLatitude())).append(" ").append(GPS_FORMAT.format(geo.getLongitude())); sb.append(" HEADING:").append(getHeading()); sb.append(" TIME:").append(SDF.format(getTimestampMs())); - sb.append(" MSG:").append(getMessage().toHexString()); + sb.append(" UTC MSG:").append(getMessage().toHexString()); return sb.toString(); } @@ -115,7 +90,7 @@ public class L3HarrisTalkerGpsLocation extends MacStructureVendor */ public int getHeading() { - return getInt(HEADING); + return L3HarrisGPS.parseHeading(getMessage(), getOffset() + DATA_OFFSET); } /** @@ -124,7 +99,7 @@ public class L3HarrisTalkerGpsLocation extends MacStructureVendor */ public long getTimestampMs() { - return getInt(GPS_TIME) * 1000; //Convert seconds to milliseconds. + return L3HarrisGPS.parseTimestamp(getMessage(), getOffset() + DATA_OFFSET); } /** @@ -135,7 +110,8 @@ public class L3HarrisTalkerGpsLocation extends MacStructureVendor { if(mLocation == null) { - mLocation = P25Location.createFrom(getLatitude(), getLongitude()); + GeoPosition geoPosition = getGeoPosition(); + mLocation = P25Location.createFrom(geoPosition.getLatitude(), geoPosition.getLongitude()); } return mLocation; @@ -149,62 +125,13 @@ public class L3HarrisTalkerGpsLocation extends MacStructureVendor { if(mGeoPosition == null) { - mGeoPosition = new GeoPosition(getLatitude(), getLongitude()); + mGeoPosition = new GeoPosition(L3HarrisGPS.parseLatitude(getMessage(), getOffset() + DATA_OFFSET), + L3HarrisGPS.parseLongitude(getMessage(), getOffset() + DATA_OFFSET)); } return mGeoPosition; } - /** - * GPS location - latitude - * @return latitude degrees decimal - */ - public double getLatitude() - { - return (getLatitudeDegrees() + (getLatitudeMinutes() / 60.0)) * (getMessage().get(LATITUDE_HEMISPHERE + getOffset()) ? -1 : 1); - } - - /** - * Latitude degrees - */ - private double getLatitudeDegrees() - { - return getInt(LATITUDE_DEGREES); - } - - /** - * Latitude minutes - */ - private double getLatitudeMinutes() - { - return getInt(LATITUDE_MINUTES) + getInt(LATITUDE_MINUTES_FRACTIONAL) / 5000d; - } - - /** - * GPS location - longitude - * @return longitude degrees decimal - */ - public double getLongitude() - { - return (getLongitudeDegrees() + (getLongitudeMinutes() / 60.0)) * (getMessage().get(LONGITUDE_HEMISPHERE + getOffset()) ? -1 : 1); - } - - /** - * Longitude degrees - */ - private double getLongitudeDegrees() - { - return getInt(LONGITUDE_DEGREES); - } - - /** - * Longitude minutes - */ - private double getLongitudeMinutes() - { - return getInt(LONGITUDE_MINUTES) + getInt(LONGITUDE_MINUTES_FRACTIONAL) / 5000d; - } - @Override public List getIdentifiers() {