diff --git a/.idea/misc.xml b/.idea/misc.xml
index 00f52ed7b..769598250 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/identifier/alias/P25TalkerAliasIdentifier.java b/src/main/java/io/github/dsheirer/identifier/alias/P25TalkerAliasIdentifier.java
new file mode 100644
index 000000000..7ebb71728
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/identifier/alias/P25TalkerAliasIdentifier.java
@@ -0,0 +1,54 @@
+/*
+ * *****************************************************************************
+ * 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.identifier.alias;
+
+import io.github.dsheirer.protocol.Protocol;
+
+/**
+ * Talker alias value provided by the network for the current talker (ie FROM).
+ */
+public class P25TalkerAliasIdentifier extends TalkerAliasIdentifier
+{
+ /**
+ * Constructs an instance.
+ * @param value of the talker alias
+ */
+ public P25TalkerAliasIdentifier(String value)
+ {
+ super(value);
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return getValue() != null && !getValue().isEmpty();
+ }
+
+ @Override
+ public Protocol getProtocol()
+ {
+ return Protocol.APCO25;
+ }
+
+ public static P25TalkerAliasIdentifier create(String value)
+ {
+ return new P25TalkerAliasIdentifier(value);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/P25Location.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/P25Location.java
new file mode 100644
index 000000000..2a54f1800
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/P25Location.java
@@ -0,0 +1,60 @@
+/*
+ * *****************************************************************************
+ * 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.dmr.identifier;
+
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.identifier.location.LocationIdentifier;
+import io.github.dsheirer.identifier.location.Point;
+import io.github.dsheirer.protocol.Protocol;
+
+/**
+ * P25 location identifier for user radio reported GPS positions.
+ */
+public class P25Location extends LocationIdentifier
+{
+ /**
+ * Constructs an instance
+ *
+ * @param value location
+ * @param role to or from
+ */
+ public P25Location(Point value, Role role)
+ {
+ super(value, role);
+ }
+
+ @Override
+ public Protocol getProtocol()
+ {
+ return Protocol.APCO25;
+ }
+
+ /**
+ * Utility method to create a new DMR location
+ * @param latitude of the position
+ * @param longitude of the position
+ * @return constructed DMR location identifier
+ */
+ public static P25Location createFrom(double latitude, double longitude)
+ {
+ Point point = new Point(latitude, longitude);
+ return new P25Location(point, Role.FROM);
+ }
+}
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 f35a4ea70..d1b88213a 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
@@ -41,6 +41,7 @@
import io.github.dsheirer.module.decode.DecoderType;
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.p25.P25DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
@@ -87,8 +88,12 @@
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelGrantUpdateExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
+import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisGpsLocation;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
+import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisTalkerAlias;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
+import io.github.dsheirer.protocol.Protocol;
+import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -392,6 +397,15 @@ private void processMacMessage(MacMessage message)
case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED:
broadcast(message, mac, getCurrentChannel(), DecodeEventType.COMMAND, "UNIT REGISTRATION");
break;
+ case PHASE1_128_L3HARRIS_GPS_LOCATION:
+ processL3HarrisGps(message, mac);
+ break;
+ case PHASE1_168_L3HARRIS_TALKER_ALIAS:
+ if(mac instanceof L3HarrisTalkerAlias talkerAlias)
+ {
+ getIdentifierCollection().update(talkerAlias.getAlias());
+ }
+ break;
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
if(mac instanceof L3HarrisRegroupCommand regroup)
{
@@ -587,6 +601,25 @@ private void processIPMWP(MacMessage message, MacStructure mac) {
}
}
+ /**
+ * Broadcasts the L3Harris gps position report as a mappable/plottable event.
+ */
+ private void processL3HarrisGps(MacMessage message, MacStructure structure)
+ {
+ if(structure instanceof L3HarrisGpsLocation gps)
+ {
+ MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(gps);
+
+ broadcast(PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
+ .protocol(Protocol.APCO25)
+ .location(gps.getGeoPosition())
+ .channel(getCurrentChannel())
+ .details(gps.getLocation().toString() + " " + new Date(gps.getTimestampMs()))
+ .identifiers(collection)
+ .build());
+ }
+ }
+
private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor currentChannel, DecodeEventType eventType, String details) {
MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(mac);
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 bb19f9d52..9f1a54177 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
@@ -85,6 +85,7 @@
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownStructure;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownVendorMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
+import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisTalkerAlias;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
@@ -290,6 +291,8 @@ public static MacStructure createMacStructure(CorrectedBinaryMessage message, in
return new AdjacentStatusBroadcastAbbreviated(message, offset);
case PHASE1_125_IDENTIFIER_UPDATE:
return new FrequencyBandUpdate(message, offset);
+ case PHASE1_168_L3HARRIS_TALKER_ALIAS:
+ return new L3HarrisTalkerAlias(message, offset);
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
return new L3HarrisRegroupCommand(message, offset);
case PHASE1_192_GROUP_VOICE_CHANNEL_GRANT_EXTENDED:
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 47516b455..59d6293cf 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
@@ -76,6 +76,8 @@ public enum MacOpcode
PHASE1_123_NETWORK_STATUS_BROADCAST_ABBREVIATED(123, "NETWORK STATUS BROADCAST ABBREVIATED", 11),
PHASE1_124_ADJACENT_STATUS_BROADCAST_ABBREVIATED(124, "ADJACENT STATUS BROADCAST ABBREVIATED", 9),
PHASE1_125_IDENTIFIER_UPDATE(125, "IDENTIFIER UPDATE", 9),
+ PHASE1_128_L3HARRIS_GPS_LOCATION(128, "GPS LOCATION", -1),
+ PHASE1_168_L3HARRIS_TALKER_ALIAS(168, "TALKER ALIAS", -1),
PHASE1_176_L3HARRIS_GROUP_REGROUP(176, "REGROUP COMMAND", 17),
PHASE1_PARTITION_1_UNKNOWN_OPCODE(-1, "UNKNOWN PHASE 1 OPCODE", -1),
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGpsLocation.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGpsLocation.java
new file mode 100644
index 000000000..4b274f850
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisGpsLocation.java
@@ -0,0 +1,302 @@
+/*
+ * *****************************************************************************
+ * 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.phase2.message.mac.structure.l3harris;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+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.MacStructure;
+import io.github.dsheirer.module.decode.p25.reference.Vendor;
+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 GPS Location.
+ *
+ * Bit field definitions are best-guess from observed samples.
+ */
+public class L3HarrisGpsLocation extends MacStructure
+{
+ 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");
+ private static final DecimalFormat FIXED = new DecimalFormat("000");
+ private static final int[] OPCODE = {0, 1, 2, 3, 4, 5, 6, 7};
+ private static final int[] UNKNOWN = {8, 9, 10, 11, 12, 13, 14, 15};
+ private static final int[] VENDOR = {16, 17, 18, 19, 20, 21, 22, 23};
+ private static final int[] LENGTH = {24, 25, 26, 27, 28, 29, 30, 31}; //Length is 17.5 bytes ... observed 17 here
+
+ //Bits 32 & 33 not set in sample data - seems unused for a 1/5000th of a minute number system
+ private static final int[] LATITUDE_MINUTES_FRACTIONAL = {32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46};
+ private static final int LATITUDE_HEMISPHERE = 48;
+ //Bit 47 & 49 not set in sample data
+ private static final int[] LATITUDE_MINUTES = {50, 51, 52, 53, 54, 55};
+ private static final int[] LATITUDE_DEGREES = {56, 57, 58, 59, 60, 61, 62, 63};
+
+ //Bits 64 & 65 not set in sample data - seems unused for a 1/5000th of a minute number system
+ private static final int[] LONGITUDE_MINUTES_FRACTIONAL = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78};
+ private static final int LONGITUDE_HEMISPHERE = 80;
+ //Bit 79 & 81 not set in sample data
+ private static final int[] LONGITUDE_MINUTES = {82, 83, 84, 85, 86, 87};
+ private static final int[] LONGITUDE_DEGREES = {88, 89, 90, 91, 92, 93, 94, 95};
+
+ //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 int[] GPS_TIME = {96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111};
+
+ private static final int[] U2 = {112, 113, 114, 115, 116, 117, 118, 119};
+ private static final int[] U3 = {120, 121, 122, 123, 124, 125, 126, 127};
+ private static final int[] U4 = {128, 129, 130, 131, 132, 133, 134, 135};
+ private static final int[] U5 = {136, 137, 138, 139, 140, 141, 142, 143};
+ private static final int[] U6 = {144, 145, 146, 147, 148, 149, 150, 151};
+ private static final int[] U7 = {152, 153, 154, 155, 156, 157, 158, 159};
+ private static final int[] U8 = {160, 161, 162, 163, 164, 165, 166, 167};
+
+ private P25Location mLocation;
+ private List mIdentifiers;
+ private GeoPosition mGeoPosition;
+
+ /**
+ * Constructs the message
+ *
+ * @param message containing the message bits
+ * @param offset into the message for this structure
+ */
+ public L3HarrisGpsLocation(CorrectedBinaryMessage message, int offset)
+ {
+ super(message, offset);
+ }
+
+ /**
+ * GPS Position time in milliseconds.
+ * @return time in ms UTC
+ */
+ public long getTimestampMs()
+ {
+ return getMessage().getInt(GPS_TIME, getOffset()) * 1000; //Convert seconds to milliseconds.
+ }
+
+ /**
+ * GPS Location
+ * @return location in decimal degrees
+ */
+ public P25Location getLocation()
+ {
+ if(mLocation == null)
+ {
+ mLocation = P25Location.createFrom(getLatitude(), getLongitude());
+ }
+
+ return mLocation;
+ }
+
+ /**
+ * Geo position
+ * @return position
+ */
+ public GeoPosition getGeoPosition()
+ {
+ if(mGeoPosition == null)
+ {
+ mGeoPosition = new GeoPosition(getLatitude(), getLongitude());
+ }
+
+ 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 getMessage().getInt(LATITUDE_DEGREES, getOffset());
+ }
+
+ /**
+ * Latitude minutes
+ */
+ private double getLatitudeMinutes()
+ {
+ return getMessage().getInt(LATITUDE_MINUTES, getOffset()) +
+ getMessage().getInt(LATITUDE_MINUTES_FRACTIONAL, getOffset()) / 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 getMessage().getInt(LONGITUDE_DEGREES, getOffset());
+ }
+
+ /**
+ * Longitude minutes
+ */
+ private double getLongitudeMinutes()
+ {
+ return getMessage().getInt(LONGITUDE_MINUTES, getOffset()) +
+ getMessage().getInt(LONGITUDE_MINUTES_FRACTIONAL, getOffset()) / 5000d;
+ }
+
+ /**
+ * Textual representation of this message
+ */
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(getVendor() == Vendor.HARRIS)
+ {
+ sb.append("L3H TALKER GPS ");
+ }
+ else
+ {
+ sb.append("VENDOR:").append(getVendor()).append(" TALKER GPS ");
+ }
+
+ sb.append(GPS_FORMAT.format(getLatitude())).append(" ").append(GPS_FORMAT.format(getLongitude()));
+ sb.append(" TIME:").append(SDF.format(getTimestampMs()));
+ return sb.toString();
+ }
+
+ /**
+ * Vendor ID. This should be L3Harris unless another vendor is also using this Opcode.
+ */
+ public Vendor getVendor()
+ {
+ return Vendor.fromValue(getMessage().getInt(VENDOR, getOffset()));
+ }
+
+ /**
+ * Message length.
+ *
+ * @return length in bytes, including the opcode.
+ */
+ public int getLength()
+ {
+ return getMessage().getInt(LENGTH, getOffset());
+ }
+
+ @Override
+ public List getIdentifiers()
+ {
+ if(mIdentifiers == null)
+ {
+ mIdentifiers = new ArrayList<>();
+ mIdentifiers.add(getLocation());
+ }
+
+ return mIdentifiers;
+ }
+
+ public static void main(String[] args)
+ {
+ String[] examples = new String[]{
+ "8080AAA41106CC031E150E8A53C0E69200061C05E00D9000",
+ "8080AAA41106CC031E150E8A53C0E89300061C2A700D9000",
+ "8080AAA41106CC031E150E8A53C0E99300061C78400D9000",
+ "8080AAA41106D6031E150E8A53C0F29300061CDB200D9000",
+ "8080AAA41106D6031E150E8A53C0F39300061C89100D9000",
+ "8C80AAA41106CC031E150E8A53C11D9800061CE68088C000",
+ "8C80AAA41106CC031E150E8A53C11E9800061C10D088C000",
+ "8880AAA41106CC031E150E8A53C0D39100061C6AF0732000",
+ "8080AAA4111F7C231D0852B152BBB99600001C75700D9000",
+ "8080AAA4111F7C231D0852B152BBBA9700001CC3700D9000",
+ "8480AAA4111F86231D0852B152BBE08D00001C8BD0F67000",
+ "8480AAA4111F86231D0852B152BBE18D00001CD9E0F67000",
+ "8480AAA4111F7C231D0852B152BBEA8C00001C6560F67000",
+ "8480AAA4111F7C231D0852B152BBEB8D00001C7700F67000",
+ "8480AAA4111F7C231D0852B152BBEE8D00001CE480F67000",
+ "8480AAA4111F7C231D0852B152BBF88A00001CDDB0F67000",
+ "8480AAA4111F7C231D0852B152BBF98A00001C8F80F67000",
+ "8480AAA4111F7C231D0852B152BBF88D00001C9570F67000",
+ "8880AAA4111748212525C6B84DBD9C0F080517E600732000", //19 04642010
+ "8880AAA4111748212525C6B84DBDA69F0A08172480732000",
+ "8880AAA4111748212525C6B84DBDA89F0A08174B40732000",
+ "8880AAA4111748212525C6B84DBDAAB4010816B9E0732000",
+ "8880AAA4111748212525C6B84DBDABB401081762A0732000",
+ "8880AAA4111748212525C6B84DBDAC9C0008170650732000",
+ "8C80AAA4111734212525DAB84DBE454200071748A088C000",
+ "9080AAA4111734212525DAB84DBE4E42000717B4E0F0F000",
+ "9080AAA4111734212525DAB84DBE4F42000717E6D0F0F000",
+ "8480AAA4111748212525D0B84DBE868E180217A0A0F67000",
+ "8480AAA4111748212525C6B84DBE87641303170E80F67000",
+ "8480AAA411173E212525BCB84DBE89641303172190F67000",
+ "8480AAA41116A821252468B84DC0C3CF3A0915ECF0F67000",
+ "8480AAA41116A82125247CB84DC0C443130916FBA0F67000",
+ "8480AAA41116A821252486B84DC0C7272005133230F67000",
+ "9080AAA41116BC2325178EB24DBE2855050D15E920F0F000", //04642051
+ "8C80AAA41116BC2325178EB24DBE2E71060D142C1088C000",
+ "8C80AAA41116BC2325178EB24DBE2F73030D141C1088C000",
+ "8480AAA411173E232517D4B24DBF03C1020D14B910F67000",
+ "9080AAA411260C21251A90804EBF8A9CAD1A166980F0F000",//04642016 << only sample set that is moving, heading south then north
+ "9080AAA411261621251B1C804EBF8B8C921A16FAA0F0F000",
+ "8480AAA411263421250F6E814EBFD3982F13168E80F67000",
+ "8480AAA411263421250EEC814EBFD4982F1316FC70F67000",
+ "8480AAA411263421250E60814EBFD5982F131645A0F67000",
+ "8880AAA411087A21251F0E804EC07CB39108143C40732000",
+ "8880AAA411087A21251F54804EC07DB39108140EE0732000",
+ "8480AAA4110820212520EE804EC0AA4ED00116E970F67000",
+ "8480AAA411083E21252094804EC0AB26F702164140F67000",
+ "8480AAA411086621252058804EC0AC26F702169920F67000",
+ "8480AAA411088E2125201C804EC0AD26F702167A00F67000",
+ "8480AAA41108B621251FD6804EC0AEEFE50116DDB0F67000",
+ "8C80AAA4110AD21F25134C804EBFCDBD040A1801F088C000", //50 04642025
+ "8C80AAA4110AD21F25134C804EBFCEBD090A1832C088C000",
+ "8080AAA4110AFA1F25136A804EBFE9A70B0A18A4F00D9000",
+ "9080AAA4111BE41E250DD4AE4DC09B5F010314D660F0F000", //53 04642031
+ "9080AAA4111BE41E250DD4AE4DC09D5F0903149440F0F000",
+ "9080AAA4111BE41E250DD4AE4DC09E5F0903146210F0F000",
+ "8080AAA4111BBC1E250DF2AE4DC0A3022F0714FA900D9000",
+ "8080AAA4111BBC1E250DE8AE4DC0A4022F071484F00D9000",
+ "8080AAA4111BD01E250DE8AE4DC0A5022F07146DB00D9000",
+ };
+
+ for(String example: examples)
+ {
+ CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.loadHex(example));
+ L3HarrisGpsLocation gps = new L3HarrisGpsLocation(cbm, 8);
+ System.out.println(gps);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerAlias.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerAlias.java
new file mode 100644
index 000000000..950521a6c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/l3harris/L3HarrisTalkerAlias.java
@@ -0,0 +1,118 @@
+/*
+ * *****************************************************************************
+ * 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.phase2.message.mac.structure.l3harris;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier;
+import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
+import io.github.dsheirer.module.decode.p25.reference.Vendor;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * L3Harris Talker Alias.
+ */
+public class L3HarrisTalkerAlias extends MacStructure
+{
+ private static final int[] VENDOR = {8, 9, 10, 11, 12, 13, 14, 15};
+ private static final int[] LENGTH = {16, 17, 18, 19, 20, 21, 22, 23};
+ private static final int ALIAS_START = 24;
+
+ private List mIdentifiers;
+ private P25TalkerAliasIdentifier mAliasIdentifier;
+
+ /**
+ * Constructs the message
+ *
+ * @param message containing the message bits
+ * @param offset into the message for this structure
+ */
+ public L3HarrisTalkerAlias(CorrectedBinaryMessage message, int offset)
+ {
+ super(message, offset);
+ }
+
+ /**
+ * Textual representation of this message
+ */
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(getVendor() == Vendor.HARRIS)
+ {
+ sb.append("L3HARRIS ");
+ }
+ else
+ {
+ sb.append("WARNING: UNKNOWN VENDOR:").append(getVendor());
+ }
+
+ sb.append(" TALKER ALIAS: ");
+ sb.append(getAlias());
+ return sb.toString();
+ }
+
+ /**
+ * Alias identifier
+ */
+ public P25TalkerAliasIdentifier getAlias()
+ {
+ if(mAliasIdentifier == null)
+ {
+ int length = getLength() * 8;
+ String alias = new String(getMessage().getSubMessage(ALIAS_START + getOffset(), length + getOffset()).getBytes()).trim();
+ mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
+ }
+
+ return mAliasIdentifier;
+ }
+
+ /**
+ * Vendor ID. This should be L3Harris unless another vendor is also using this Opcode.
+ */
+ public Vendor getVendor()
+ {
+ return Vendor.fromValue(getMessage().getInt(VENDOR, getOffset()));
+ }
+
+ /**
+ * Message length.
+ *
+ * @return length in bytes, including the opcode.
+ */
+ public int getLength()
+ {
+ return getMessage().getInt(LENGTH, getOffset());
+ }
+
+ @Override
+ public List getIdentifiers()
+ {
+ if(mIdentifiers == null)
+ {
+ mIdentifiers = new ArrayList<>();
+ mIdentifiers.add(getAlias());
+ }
+
+ return mIdentifiers;
+ }
+}