#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
@@ -95,25 +95,44 @@ public class PatchGroup
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
* @return true if the patched group identifier is new and was added
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
public boolean addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
boolean added = false;
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{
mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
added |= mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
}
return added;
}
/**
* Removes the patched talkgroup from this patch group.
* @param talkgroup to remove
*/
public void removePatchedTalkgroup(TalkgroupIdentifier talkgroup)
{
mPatchedTalkgroupIdentifiers.remove(talkgroup);
}
/**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
public boolean addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{
boolean added = false;
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{
addPatchedTalkgroup(identifier);
added |= addPatchedTalkgroup(identifier);
}
return added;
}
/**
@@ -129,24 +148,41 @@ public class PatchGroup
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
public boolean addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
return true;
}
return false;
}
/**
* Removes the patched radio from this patch group.
* @param radioIdentifier to remove
*/
public void removePatchedRadio(RadioIdentifier radioIdentifier)
{
mPatchedRadioIdentifiers.remove(radioIdentifier);
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
* @return true if any new identifiers were added
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
public boolean addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
boolean added = false;
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
added |= addPatchedRadio(identifier);
}
return added;
}
/**

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
@@ -22,11 +22,14 @@ package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Manager for (temporary) patch groups aka super groups. This manager monitors patch group additions and deletions and
@@ -39,7 +42,8 @@ import java.util.Map;
*/
public class PatchGroupManager
{
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
private static final long PATCH_GROUP_FRESHNESS_THRESHOLD_MS = Duration.ofSeconds(30).toMillis();
private Map<Integer,PatchGroupTracker> mPatchGroupTrackerMap = new HashMap<>();
/**
* Constructs an instance
@@ -48,12 +52,36 @@ public class PatchGroupManager
{
}
/**
* Summary listing of active patch groups
*/
public String getPatchGroupSummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Active Patch Groups\n");
List<PatchGroupTracker> trackers = new ArrayList<>(mPatchGroupTrackerMap.values());
if(trackers.isEmpty())
{
sb.append("None");
}
else
{
for(PatchGroupTracker tracker : trackers)
{
sb.append(" ").append(tracker.mPatchGroupIdentifier).append("\n");
}
}
return sb.toString();
}
/**
* Clears any existing patch groups
*/
public void clear()
{
mPatchGroupMap.clear();
mPatchGroupTrackerMap.clear();
}
/**
@@ -63,46 +91,53 @@ public class PatchGroupManager
* existing patch group is discarded and replaced with the updated version of the patch group.
*
* @param patchGroupIdentifier to add or update
* @return true if the group was added or updated
*/
public synchronized void addPatchGroup(PatchGroupIdentifier patchGroupIdentifier)
public synchronized boolean addPatchGroup(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
PatchGroup update = patchGroupIdentifier.getValue();
if(update.getPatchGroup().getValue() > 0)
{
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
{
PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
int patchGroup = update.getPatchGroup().getValue();
//If the patch group version number is the same, this is an update
if(existingPatchGroup.getVersion() == update.getVersion())
if(patchGroup > 0)
{
if(mPatchGroupTrackerMap.containsKey(patchGroup))
{
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(patchGroup);
if(tracker.isStale(timestamp))
{
existingPatchGroup.addPatchedTalkgroups(update.getPatchedTalkgroupIdentifiers());
existingPatchGroup.addPatchedRadios(update.getPatchedRadioIdentifiers());
//Replace the existing patch group if it is stale.
mPatchGroupTrackerMap.put(patchGroup, new PatchGroupTracker(patchGroupIdentifier, timestamp));
return true;
}
//Otherwise, this is a replace operation.
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
//Update the existing patch group.
return tracker.add(patchGroupIdentifier, timestamp);
}
}
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
mPatchGroupTrackerMap.put(patchGroup, new PatchGroupTracker(patchGroupIdentifier, timestamp));
return true;
}
}
return false;
}
/**
* Adds any patch group identifiers contained in the list.
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
*/
public synchronized void addPatchGroups(List<Identifier> identifiers)
public synchronized void addPatchGroups(List<Identifier> identifiers, long referenceTimestamp)
{
for(Identifier identifier : identifiers)
{
if(identifier instanceof PatchGroupIdentifier)
{
addPatchGroup((PatchGroupIdentifier)identifier);
addPatchGroup((PatchGroupIdentifier)identifier, referenceTimestamp);
}
}
}
@@ -111,11 +146,12 @@ public class PatchGroupManager
* Removes the patch group from this manager if it is currently being managed.
*
* @param patchGroupIdentifier to remove
* @return true if the patch group was removed.
*/
public synchronized void removePatchGroup(PatchGroupIdentifier patchGroupIdentifier)
public synchronized boolean removePatchGroup(PatchGroupIdentifier patchGroupIdentifier)
{
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
mPatchGroupMap.remove(id);
return mPatchGroupTrackerMap.remove(id) != null;
}
/**
@@ -136,15 +172,16 @@ public class PatchGroupManager
* Updates the list of identifiers by replacing any talkgroups or patch groups with the current
* version of the patch group and a complete listing of the patched talkgroups.
* @param identifiers to update
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
* @return list of identifiers
*/
public List<Identifier> update(List<Identifier> identifiers)
public List<Identifier> update(List<Identifier> identifiers, long referenceTimestamp)
{
List<Identifier> updated = new ArrayList<>();
for(Identifier identifier: identifiers)
{
updated.add(update(identifier));
updated.add(update(identifier, referenceTimestamp));
}
return updated;
@@ -155,9 +192,10 @@ public class PatchGroupManager
* if the identifier matches a currently managed patch group.
*
* @param identifier for a talkgroup or a patch group.
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
* @return current patch group or the original identifier
*/
public Identifier update(Identifier identifier)
public Identifier update(Identifier identifier, long referenceTimestamp)
{
if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{
@@ -166,22 +204,49 @@ public class PatchGroupManager
case TALKGROUP:
if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
Identifier mapValue = mPatchGroupMap.get(talkgroupIdentifier.getValue());
if (mapValue != null)
int id = talkgroupIdentifier.getValue();
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(id);
if(tracker != null)
{
return mapValue;
if(tracker.isStale(referenceTimestamp))
{
mPatchGroupTrackerMap.remove(id);
}
else
{
//Perform substitution - return patch group instead of the original talkgroup
return tracker.getPatchGroupIdentifier(referenceTimestamp);
}
}
}
break;
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
if (mapValue != null)
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(id);
if(tracker != null)
{
return mapValue;
if(tracker.isStale(referenceTimestamp))
{
mPatchGroupTrackerMap.put(id, new PatchGroupTracker(patchGroupIdentifier, referenceTimestamp));
mPatchGroupTrackerMap.remove(id);
}
else if(tracker.getPatchGroupIdentifier(referenceTimestamp).getValue().getVersion() !=
patchGroupIdentifier.getValue().getVersion())
{
mPatchGroupTrackerMap.put(id, new PatchGroupTracker(patchGroupIdentifier, referenceTimestamp));
mPatchGroupTrackerMap.remove(id);
}
else
{
//Perform substitution - return patch group instead of the original talkgroup
return tracker.getPatchGroupIdentifier(referenceTimestamp);
}
}
}
break;
@@ -190,4 +255,107 @@ public class PatchGroupManager
return identifier;
}
/**
* Tracks the freshness of a patch group and any member talkgroup and radio identifiers to ensure that stale
* patch group identifiers are removed.
*/
public class PatchGroupTracker
{
private PatchGroupIdentifier mPatchGroupIdentifier;
private long mLastUpdateTimestamp;
private Map<TalkgroupIdentifier,Long> mTalkgroupTimestampMap = new HashMap<>();
private Map<RadioIdentifier,Long> mRadioTimestampMap = new HashMap<>();
/**
* Constructs an instance
* @param patchGroupIdentifier
*/
public PatchGroupTracker(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
mPatchGroupIdentifier = patchGroupIdentifier;
add(patchGroupIdentifier, timestamp);
}
/**
* Patch group monitored by this tracker. This method will cleanup and remove any stale radio or talkgroup
* identifiers on access.
*
* Note: this tracker should be checked for freshness by first checking the isStale() method before accessing
* the tracked patch group by this method.
*
* @param referenceTimestamp for comparing stored radio and talkgroup values for staleness.
* @return current version of the talkgroup
*/
public PatchGroupIdentifier getPatchGroupIdentifier(long referenceTimestamp)
{
//Remove stale talkgroups
List<Map.Entry<TalkgroupIdentifier,Long>> toRemove = mTalkgroupTimestampMap.entrySet().stream()
.filter(entry -> isStale(entry.getValue(), referenceTimestamp)).collect(Collectors.toList());
for(Map.Entry<TalkgroupIdentifier,Long> entry: toRemove)
{
mTalkgroupTimestampMap.remove(entry.getKey());
mPatchGroupIdentifier.getValue().removePatchedTalkgroup(entry.getKey());
}
//Remove stale radios
List<Map.Entry<RadioIdentifier,Long>> toRemove2 = mRadioTimestampMap.entrySet().stream()
.filter(entry -> isStale(entry.getValue(), referenceTimestamp)).collect(Collectors.toList());
for(Map.Entry<RadioIdentifier,Long> entry: toRemove2)
{
mRadioTimestampMap.remove(entry.getKey());
mPatchGroupIdentifier.getValue().removePatchedRadio(entry.getKey());
}
return mPatchGroupIdentifier;
}
/**
* Indicates if the stored timestamp is stale relative to the reference timestamp.
* @param storedTimestamp to compare
* @param referenceTimestamp representing the current time.
* @return true of the stored timestamp is stale.
*/
private boolean isStale(long storedTimestamp, long referenceTimestamp)
{
return referenceTimestamp - storedTimestamp > PATCH_GROUP_FRESHNESS_THRESHOLD_MS;
}
/**
* Indicates if the tracked patch group is stale and hasn't received any add/updates within a period of time.
* @param referenceTimestamp representing now for comparison against the patch groups update timestamp.
* @return true if the tracked patch group is stale.
*/
public boolean isStale(long referenceTimestamp)
{
return isStale(mLastUpdateTimestamp, referenceTimestamp);
}
/**
* Add or update the patch group and update the freshness timestamp.
* @param patchGroupIdentifier containing an add value.
*/
public boolean add(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
mLastUpdateTimestamp = timestamp;
boolean added = false;
for(TalkgroupIdentifier talkgroup: patchGroupIdentifier.getValue().getPatchedTalkgroupIdentifiers())
{
mTalkgroupTimestampMap.put(talkgroup, timestamp);
added |= mPatchGroupIdentifier.getValue().addPatchedTalkgroup(talkgroup);
}
for(RadioIdentifier radio: patchGroupIdentifier.getValue().getPatchedRadioIdentifiers())
{
mRadioTimestampMap.put(radio, timestamp);
added |= mPatchGroupIdentifier.getValue().addPatchedRadio(radio);
}
return added;
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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
@@ -27,13 +27,25 @@ import io.github.dsheirer.identifier.IdentifierCollection;
*/
public class PatchGroupPreLoadDataContent extends PreloadDataContent<IdentifierCollection>
{
private long mTimestamp;
/**
* Constructs an instance
*
* @param data to preload
*/
public PatchGroupPreLoadDataContent(IdentifierCollection data)
public PatchGroupPreLoadDataContent(IdentifierCollection data, long timestamp)
{
super(data);
mTimestamp = timestamp;
}
/**
* Freshness timestamp for the preload data.
* @return timestamp milliseconds.
*/
public long getTimestamp()
{
return mTimestamp;
}
}