Merge pull request #1793 from DSheirer/1787-dmr-mask-manager-update

#1787 Enhance DMR CSBK CRC Calculations - Auto-Detect Alternate CRC Mask Values
This commit is contained in:
Denny Sheirer
2024-01-12 07:53:01 -05:00
committed by GitHub
2 changed files with 144 additions and 89 deletions

View File

@@ -24,20 +24,38 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Manages alternate CRC masks that may be employed in a system. * Manages alternate CRC masks that may be employed in a system. Some vendors employ non-standard CRC checksum masks
* or initial fill values. This utility tracks these non-standard values by observation count and time and accepts
* these alternate mask values to automatically correct or validate messages when those messages employ these
* alternate mask values. Alternate mask values must be observed at least 3x times in a 2-minute window against the
* same message content, in order for the alternate mask value to be used. Tracked values that do not get another
* observation count within a 2-minute window will be ejected from the cache and excluded as a valid CRC mask candidate.
*
* Notes: different alternate CRC mask values can be used independently in each timeslot. Further, I've seen multiple
* CRC mask values being employed within the same timeslot. This indicates that systems can use multiple CRC mask
* values against CSBK messages and some other function is involved in determining which CRC mask value to use when
* checking the CRC of the transmitted CSBK ... fun and games!
*/ */
public class DmrCrcMaskManager public class DmrCrcMaskManager
{ {
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class); private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
private Map<Integer, ResidualCrcMaskTracker> mCsbkResidualTrackerMap = new TreeMap<>(); private Map<Integer, MaskTracker> mCsbkTrackerMapTS1 = new TreeMap<>();
private int mDominantMask = 0; private Map<Integer, MaskTracker> mCsbkTrackerMapTS2 = new TreeMap<>();
private Set<MaskTracker> mSortedTrackersTS1 = new TreeSet<>();
private Set<MaskTracker> mSortedTrackersTS2 = new TreeSet<>();
/** /**
* Constructs an instance * Constructs an instance
@@ -46,35 +64,27 @@ public class DmrCrcMaskManager
{ {
} }
/**
* Creates a deep copy (ie clone) of all tracked values
* @return list of tracker clones.
*/
public List<ResidualCrcMaskTracker> cloneTrackers()
{
List<ResidualCrcMaskTracker>trackers = new ArrayList<>();
for(ResidualCrcMaskTracker tracker: mCsbkResidualTrackerMap.values())
{
trackers.add(tracker.clone());
}
return trackers;
}
/** /**
* Logs the current tracked CRC values and counts. * Logs the current tracked CRC values and counts.
*/ */
public void log() public void log()
{ {
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
Collections.sort(trackers);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("DMR CRC Mask Manager - Current Tracked Values\n");
for(ResidualCrcMaskTracker tracker: trackers) sb.append("DMR CRC Mask Manager - TS1 Current Tracked Values\n");
for(MaskTracker tracker: mSortedTrackersTS1)
{ {
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue())); sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
sb.append(" Count:").append(tracker.getCount()); sb.append(" Count:").append(tracker.getObservationCount());
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
}
sb.append("DMR CRC Mask Manager - TS2 Current Tracked Values\n");
for(MaskTracker tracker: mSortedTrackersTS2)
{
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
sb.append(" Count:").append(tracker.getObservationCount());
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n"); sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
} }
@@ -83,104 +93,139 @@ public class DmrCrcMaskManager
/** /**
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and * Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
* if so, attempts to correct the message by using the alternate mask value once the alternate mask value is * if so, attempts to correct the message.
* detected to be used consistently enough. * @param csbk to re-check using the currently tracked mask value.
* @param csbk to re-check.
*/ */
public void check(CSBKMessage csbk) public void check(CSBKMessage csbk)
{ {
if(!csbk.isValid()) if(!csbk.isValid())
{ {
int residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80); int alternateMask = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
if(mCsbkResidualTrackerMap.containsKey(residual)) Map<Integer,MaskTracker> map;
Set<MaskTracker> sortedSet;
if(csbk.getTimeslot() == 1)
{ {
mCsbkResidualTrackerMap.get(residual).increment(); map = mCsbkTrackerMapTS1;
sortedSet = mSortedTrackersTS1;
} }
else else
{ {
mCsbkResidualTrackerMap.put(residual, new ResidualCrcMaskTracker(residual)); map = mCsbkTrackerMapTS2;
sortedSet = mSortedTrackersTS2;
} }
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values()); if(map.containsKey(alternateMask))
Collections.sort(trackers);
if(trackers.size() > 5)
{ {
mCsbkResidualTrackerMap.remove(trackers.get(0).getTrackedValue()); MaskTracker matchingTracker = map.get(alternateMask);
} matchingTracker.increment(csbk.getTimestamp());
if(trackers.size() > 0) if(matchingTracker.isValid())
{
ResidualCrcMaskTracker dominant = trackers.get(trackers.size() - 1);
if(dominant.isValid())
{ {
mDominantMask = dominant.getTrackedValue(); csbk.checkCRC(matchingTracker.getTrackedValue());
} }
} }
else
if(mDominantMask != 0)
{ {
csbk.checkCRC(mDominantMask); //Attempt to use other tracked mask values in order of observed count and also remove stale trackers
// boolean found = false;
// if(csbk.isValid()) MaskTracker next;
// { Iterator<MaskTracker> it = sortedSet.iterator();
// LOGGER.info("CSBK fixed using alternate mask: " + Integer.toHexString(mDominantMask).toUpperCase()); while(it.hasNext())
// } {
next = it.next();
//If the tracker is stale, remove it
if(next.isStale(csbk.getTimestamp()))
{
it.remove();
map.remove(next.getTrackedValue());
}
else
{
//If we haven't found a match yet, attempt to correct the message using this tracked mask value
if(!found)
{
csbk.checkCRC(next.getTrackedValue());
if(csbk.isValid())
{
found = true;
next.increment(csbk.getTimestamp());
}
}
}
}
//If we didn't find a perfect match or even a close match (with 1 bit error), create a new tracked value.
if(!found)
{
MaskTracker tracker = new MaskTracker(alternateMask, csbk.getTimestamp());
map.put(alternateMask, tracker);
sortedSet.add(tracker);
}
} }
} }
} }
/** /**
* Residual CRC mask value tracker. Tracks the number of observances of a residual CRC calculated value to detect * Alternate CRC mask value tracker. Tracks the number of observances of the mask value to detect when an alternate
* when an alternate CRC mask pattern is employed and automatically correct CRC values for messages. * CRC mask pattern is employed and can then be used to automatically correct CRC values for messages.
*/ */
public class ResidualCrcMaskTracker implements Comparable<ResidualCrcMaskTracker> public class MaskTracker implements Comparable<MaskTracker>
{ {
private static final long STALENESS_TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
private static final int STALENESS_COUNT_THRESHOLD = 100;
private int mTrackedValue; private int mTrackedValue;
private int mCount; private int mObservationCount;
private int mStalenessCount;
private long mLastUpdated; private long mLastUpdated;
/** /**
* Constructs an instance for the specified residual, sets the count to one and sets the last update to now. * Constructs an instance for the specified mask, sets the observation count to one and sets the last update to
* the supplied timestamp.
* @param residual value to track. * @param residual value to track.
* @param timestamp for the observation
*/ */
public ResidualCrcMaskTracker(int residual) public MaskTracker(int residual, long timestamp)
{ {
mTrackedValue = residual; mTrackedValue = residual;
mCount = 1; mObservationCount = 1;
mLastUpdated = System.currentTimeMillis(); mLastUpdated = timestamp;
} }
/** /**
* Create a deep copy of this instance. * Count of observations for this mask value.
* @return cloned instance.
*/
public ResidualCrcMaskTracker clone()
{
ResidualCrcMaskTracker clone = new ResidualCrcMaskTracker(getTrackedValue());
clone.mCount = mCount;
clone.mLastUpdated = mLastUpdated;
return clone;
}
/**
* Count of number of times this residual has been observed.
* @return count * @return count
*/ */
public int getCount() public int getObservationCount()
{ {
return mCount; return mObservationCount;
} }
/** /**
* Indicates that the residual value has been observed at least 3x times indicating that it is possibly valid. * Indicates that the residual value has been observed at least 3x times within the preceding 2 minute window,
* @return * indicating that it is (likely) valid.
* @return true if the tracked mask value is valid.
*/ */
public boolean isValid() public boolean isValid()
{ {
return mCount >= 3; return mObservationCount >= 3;
}
/**
* Indicates if this tracker is stale, relative to the supplied timestamp. Stale indicates that this tracked
* value hasn't been observed in the preceding 2 minutes, or has been checked for staleness more than 100 times
* without an updated observation.
* @param timestamp to compare for staleness check.
* @return true if this tracker is stale.
*/
public boolean isStale(long timestamp)
{
mStalenessCount++;
return mStalenessCount > STALENESS_COUNT_THRESHOLD ||
Math.abs(timestamp - mLastUpdated) > STALENESS_TIME_THRESHOLD;
} }
/** /**
@@ -192,38 +237,44 @@ public class DmrCrcMaskManager
} }
/** /**
* Residual tracked value. * Tracked alternate mask value.
* @return residual * @return tracked mask value
*/ */
public int getTrackedValue() public int getTrackedValue()
{ {
return mTrackedValue; return mTrackedValue;
} }
public void increment() /**
* Increment the observation count and use the supplied timestamp as the last updated timestamp.
* @param timestamp for the observed message.
*/
public void increment(long timestamp)
{ {
mCount++; mStalenessCount = 0;
//Prevent rollover by keeping value at max integer value. mObservationCount++;
if(mCount < 0) //Detect integer rollover and apply max integer value.
if(mObservationCount < 0)
{ {
mCount = Integer.MAX_VALUE; mObservationCount = Integer.MAX_VALUE;
} }
mLastUpdated = System.currentTimeMillis(); mLastUpdated = timestamp;
} }
/** /**
* Custom sort order by count and then by last updated timestamp. * Custom (reversed) sort order by largest count and then by latest updated timestamp.
* @param o the object to be compared. * @param o the object to be compared.
* @return * @return comparison value.
*/ */
@Override @Override
public int compareTo(ResidualCrcMaskTracker o) public int compareTo(MaskTracker o)
{ {
int comparison = Integer.compare(getCount(), o.getCount()); //Multiply by -1 to apply a reverse ordering
int comparison = Integer.compare(getObservationCount(), o.getObservationCount()) * -1;
if(comparison == 0) if(comparison == 0)
{ {
comparison = Long.compare(getLastUpdated(), o.getLastUpdated()); comparison = Long.compare(getLastUpdated(), o.getLastUpdated()) * -1;
} }
return comparison; return comparison;

View File

@@ -151,6 +151,10 @@ public class AdjacentSiteInformation extends Announcement
} }
sb.append("CC:").append(getSlotType().getColorCode()); sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" ").append(getSystemIdentityCode().getModel()); sb.append(" ").append(getSystemIdentityCode().getModel());
sb.append(" NEIGHBOR NETWORK:").append(getNeighborSystemIdentityCode().getNetwork()); sb.append(" NEIGHBOR NETWORK:").append(getNeighborSystemIdentityCode().getNetwork());
sb.append(" SITE:").append(getNeighborSystemIdentityCode().getSite()); sb.append(" SITE:").append(getNeighborSystemIdentityCode().getSite());