#1758 Adds P25 Phase 2 L3Harris talker alias and GPS report.

This commit is contained in:
Dennis Sheirer
2023-12-10 09:43:31 -05:00
parent 9b89e80bb5
commit 93c61c48f9
8 changed files with 573 additions and 1 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}
}

View File

@@ -41,6 +41,7 @@ import io.github.dsheirer.message.IMessage;
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.UnitToU
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 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
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 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
}
}
/**
* 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);

View File

@@ -85,6 +85,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToU
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 class MacMessageFactory
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:

View File

@@ -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),

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<Identifier> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getAlias());
}
return mIdentifiers;
}
}