#1304 P25 Phase 1/2 decoder enhancements. (#1898)

* P25 Phase 2 now supports control channel decoding and decoder was enhanced to improve decodes.
* Radio reference site editor updated for phase 2 sites to allow user to select FDMA or TDMA control.
* Decode events are now minimized and de-duplicated.
* Fully parsing all phase 1/2 messages from the latest ICD.
* Vendor specific messaging recoveries (Moto & Harris).
* Patch group management enhancements with stale patch detection.
* Tuner editor now fully resets the min/max frequency values
* Default traffic channel count increased to 20 (from 3).
* ISSI roaming radio and talkgroup values now displayed and aliasable as fully qualified values (e.g. 1234(123.456.7890)
* Moto emergency alarm activation messaging support
* Expanded list of P25 encryption algorithms
* Message (bits) viewer enhanced for P25 phase 1 and phase 2
* IPV4 addresses and UDP ports no longer classified as user values - won't display in event views.
* New framework for describing message binary fields that should reduce overall memory footprint and enhance code readability

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2024-04-26 13:55:17 -04:00
committed by GitHub
parent f5938fa8e2
commit 7e709d9d6f
430 changed files with 30098 additions and 13300 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -50,6 +50,28 @@ public class DecodeEvent implements IDecodeEvent
mTimeStart = start;
}
/**
* Sets or changes the decode event type.
* @param type to set.
*/
public void setDecodeEventType(DecodeEventType type)
{
if(type == null)
{
throw new IllegalArgumentException("Decode event type cannot be null");
}
mDecodeEventType = type;
}
/**
* Creates a snapshot of this decode event for offline analysis
*/
public DecodeEventSnapshot getSnapshot()
{
return new DecodeEventSnapshot(this);
}
/**
* Creates a new decode event builder with the specified start timestamp.
* @param decodeEventType for the event
@@ -238,7 +260,8 @@ public class DecodeEvent implements IDecodeEvent
sb.append(" IDS:").append(Joiner.on(",").join(mIdentifierCollection.getIdentifiers()));
}
sb.append(" DURATION:").append(getDuration());
sb.append(" CHANNEL:").append(mChannelDescriptor);
sb.append(" CHANNEL:").append(getChannelDescriptor());
sb.append(" TIMESLOT:").append(getTimeslot());
return sb.toString();
}

View File

@@ -0,0 +1,136 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.event;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Provides duplicate decode event detection support
*/
public class DecodeEventDuplicateDetector
{
private static final long EVENT_MAX_AGE_MILLISECONDS = Duration.ofMinutes(1).toMillis();
private Map<DecodeEventType,DecodeEventTracker> mTrackerMap = new HashMap<>();
/**
* Indicates if the event is a duplicate event.
* @param event to check
* @param timestamp of a current message to trigger time-based event age off.
* @return true if the event is a duplicate.
*/
public synchronized boolean isDuplicate(IDecodeEvent event, long timestamp)
{
//Null event types and voice call event types are not tracked by this detector
if(event.getEventType() == null || event.getEventType().isVoiceCallEvent())
{
return false;
}
if(!mTrackerMap.containsKey(event.getEventType()))
{
mTrackerMap.put(event.getEventType(), new DecodeEventTracker());
}
boolean duplicate = mTrackerMap.get(event.getEventType()).isDuplicate(event);
for(DecodeEventTracker tracker: mTrackerMap.values())
{
tracker.ageOff(timestamp);
}
return duplicate;
}
/**
* Tracks all decode events for a given decode event type
*/
private class DecodeEventTracker
{
private Map<String,IDecodeEvent> mDecodeEventMap = new HashMap<>();
/**
* Checks the event for duplicate
* @param event to check
* @return true if duplicate
*/
public boolean isDuplicate(IDecodeEvent event)
{
if(event.getIdentifierCollection().getToIdentifier() == null || event.getDetails() == null)
{
return false;
}
String key = getKey(event);
if(key == null || key.isEmpty())
{
return false;
}
if(mDecodeEventMap.containsKey(key))
{
return true;
}
else
{
mDecodeEventMap.put(key, event);
}
return false;
}
/**
* Generates a unique event key that is a combinatio of the TO identifier value and the details of the event.
* @param event for the key
* @return generated key.
*/
private static String getKey(IDecodeEvent event)
{
return event.getIdentifierCollection().getToIdentifier().toString() + event.getDetails();
}
/**
* Removes all decode events from the duplicate detection map that are too old.
* @param timestamp for the current messaging.
*/
public void ageOff(long timestamp)
{
long threshold = timestamp - EVENT_MAX_AGE_MILLISECONDS;
List<String> toRemove = new ArrayList<>();
for(Map.Entry<String,IDecodeEvent> entry: mDecodeEventMap.entrySet())
{
if(entry.getValue() == null || entry.getValue().getTimeStart() < threshold)
{
toRemove.add(entry.getKey());
}
}
for(String key: toRemove)
{
mDecodeEventMap.remove(key);
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -115,9 +115,9 @@ public class DecodeEventModel extends ClearableHistoryModel<IDecodeEvent> implem
if(channelDescriptor != null)
{
if(event.hasTimeslot())
if(event.hasTimeslot() && !event.toString().contains("TS"))
{
return channelDescriptor + " TS:" + event.getTimeslot();
return channelDescriptor + " TS" + event.getTimeslot();
}
else
{
@@ -126,9 +126,9 @@ public class DecodeEventModel extends ClearableHistoryModel<IDecodeEvent> implem
}
else
{
if(event.hasTimeslot())
if(event.hasTimeslot() && !event.toString().contains("TS"))
{
return "TS:" + event.getTimeslot();
return "TS" + event.getTimeslot();
}
else
{

View File

@@ -0,0 +1,50 @@
package io.github.dsheirer.module.decode.event;
import io.github.dsheirer.identifier.IdentifierCollection;
/**
* Snapshot of a decode event that also preserves the object instance hash code.
*/
public class DecodeEventSnapshot extends DecodeEvent
{
private int mOriginalHashCode;
/**
* Constructs an instance
*
* @param decodeEvent to snapshot
*/
public DecodeEventSnapshot(DecodeEvent decodeEvent)
{
super(decodeEvent.getEventType(), decodeEvent.getTimeStart());
mOriginalHashCode = decodeEvent.hashCode();
setDetails(decodeEvent.getDetails());
setDuration(decodeEvent.getDuration());
setProtocol(decodeEvent.getProtocol());
setTimeslot(decodeEvent.getTimeslot());
setChannelDescriptor(decodeEvent.getChannelDescriptor());
if(decodeEvent.getIdentifierCollection() != null)
{
setIdentifierCollection(new IdentifierCollection(decodeEvent.getIdentifierCollection().getIdentifiers()));
}
}
/**
* Hashcode of the original decode event for this snapshot (not this snapshot instance's hash code).
* @return
*/
public int getOriginalHashCode()
{
return mOriginalHashCode;
}
/**
* Frequency for the channel descriptor
* @return frequency or zero.
*/
public long getFrequency()
{
return getChannelDescriptor() != null ? getChannelDescriptor().getDownlinkFrequency() : 0;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -55,6 +55,7 @@ public enum DecodeEventType
DATA_CALL_ENCRYPTED("Encrypted Data Call"),
DATA_PACKET("Data Packet"),
DEREGISTER("Deregister"),
DYNAMIC_REGROUP("Dynamic Regroup"),
EMERGENCY("EMERGENCY"),
FUNCTION("Function"),
GPS("GPS"),
@@ -106,7 +107,7 @@ public enum DecodeEventType
public static final EnumSet<DecodeEventType> COMMANDS = EnumSet.of(DecodeEventType.ANNOUNCEMENT,
DecodeEventType.STATION_ID, DecodeEventType.ACKNOWLEDGE, DecodeEventType.PAGE, DecodeEventType.QUERY,
DecodeEventType.RADIO_CHECK, DecodeEventType.STATUS, DecodeEventType.COMMAND, DecodeEventType.EMERGENCY,
DecodeEventType.NOTIFICATION, DecodeEventType.FUNCTION);
DecodeEventType.NOTIFICATION, DecodeEventType.FUNCTION, DecodeEventType.DYNAMIC_REGROUP);
/**
* Data call event types for filtering
@@ -129,6 +130,13 @@ public enum DecodeEventType
public static final EnumSet<DecodeEventType> OTHERS = EnumSet.copyOf(Arrays.stream(DecodeEventType.values())
.filter(decodeEventType -> !decodeEventType.isGrouped()).toList());
/**
* Voice call event types.
*/
public static final EnumSet<DecodeEventType> VOICE_CALL_EVENTS = EnumSet.of(CALL, CALL_ENCRYPTED, CALL_GROUP,
CALL_GROUP_ENCRYPTED, CALL_PATCH_GROUP, CALL_PATCH_GROUP_ENCRYPTED, CALL_INTERCONNECT,
CALL_INTERCONNECT_ENCRYPTED, CALL_UNIT_TO_UNIT, CALL_UNIT_TO_UNIT_ENCRYPTED);
/**
* Constructor
* @param label for the element
@@ -149,12 +157,12 @@ public enum DecodeEventType
}
/**
* Indicates if this is a call event.
* @return true if this is a call event type.
* Indicates if this is a voice call event.
* @return true if this is a voice call event type.
*/
public boolean isCallEvent()
public boolean isVoiceCallEvent()
{
return VOICE_CALLS.contains(this) || VOICE_CALLS_ENCRYPTED.contains(this) || DATA_CALLS.contains(this);
return VOICE_CALL_EVENTS.contains(this);
}
/**