#2516 DMR Decoder enhancements. Architecture now shares common C4FM framework like P25P1 and NXDN. Adds soft decoding and enhanced/corrected error detection and correction. (#2517)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-26 06:34:57 -04:00
committed by GitHub
parent 5635341d12
commit f2c98cdb40
32 changed files with 4044 additions and 1282 deletions

View File

@@ -179,6 +179,27 @@ public class C4FMEqualizer
mSymbolsUntilTraining = mDelaySymbolCount;
}
/**
* Notifies the equalizer that a sync pattern is detected and updates the sync pattern used for training. This
* supports protocols (ie DMR) that employ multiple sync patterns of the same length.
* @param syncSymbols for the detected sync pattern, which must be the same length as the constructor sync pattern.
*/
public void syncDetected(float[] syncSymbols)
{
if(syncSymbols.length != mReversedSyncSymbols.length)
{
throw new IllegalArgumentException("Sync symbols length [" + syncSymbols.length +
"] must match the equalizer sync pattern length [" + mReversedSyncSymbols.length + "]");
}
for(int x = 0; x < syncSymbols.length; x++)
{
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
}
syncDetected();
}
/**
* Indicates that the sync patter is lost and the equalizer should reset.
*/

View File

@@ -7,6 +7,26 @@ import io.github.dsheirer.dsp.symbol.Dibit;
*/
public abstract class C4FMMessageFramer
{
private static final float SOFT_SYMBOL_SCALE = (float)(4.0 / Math.PI); //Radians to symbol units (+/-1, +/-3)
/**
* Converts the soft symbol into a pair of soft bit values using max-log likelihood ratios for the Gray mapped
* 4-level symbol (D01=+3, D00=+1, D10=-1, D11=-3), where a positive soft bit value favors a 1 bit.
* @param softSymbol in radians
* @param softBits array to receive the two soft bit values
* @param index for the first bit (sign bit), followed by the second (magnitude) bit
*/
protected static void setSoftBits(float softSymbol, float[] softBits, int index)
{
float u = softSymbol * SOFT_SYMBOL_SCALE;
float magnitude = Math.abs(u);
//First bit is 1 for negative symbols
float favorZero = magnitude <= 2.0f ? 4.0f * u : 8.0f * u - 8.0f * Math.signum(u);
softBits[index] = -favorZero;
//Second bit is 1 for the outer (+/-3) symbols
softBits[index + 1] = 4.0f * (magnitude - 2.0f);
}
/**
* Process the symbol
* @param symbol in radians

View File

@@ -22,10 +22,14 @@ package io.github.dsheirer.dsp.c4fm;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.gui.viewer.sync.FMSyncResultsViewer;
import io.github.dsheirer.gui.viewer.sync.ISyncResultsListener;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
@@ -59,6 +63,9 @@ public abstract class C4FMSymbolProcessor
private final FeedbackDecoder mFeedbackDecoder;
private final C4FMMessageFramer mMessageFramer;
private final ISoftC4FMSyncDetector mSyncDetectorLagging;
//Detector (primary or lagging) that triggered the current sync detection. For multi-pattern sync detectors, this
//detector's most recently detected sync pattern is used for optimization, validation and equalizer training.
private ISoftC4FMSyncDetector mActiveSyncDetector;
private final C4FMEqualizer mSymbolEqualizer;
private static final double SYMBOL_TIMING_GAIN = 0.25;
//Clip applied to the Gardner timing error (radians squared) to limit the impact of FM discriminator clicks
@@ -68,7 +75,6 @@ public abstract class C4FMSymbolProcessor
private static final double IDEAL_OUTER_SYMBOL_PHASE = 3.0 * Math.PI / 4.0;
//Allowable gain deviation from the nominal gain, as a fraction of the nominal gain.
private static final double SAMPLE_GAIN_TOLERANCE = 0.1;
private ReferenceSyncWaveform mReferenceSyncWaveform;
private double mNoiseStandardDeviationThreshold;
private double mSamplePoint;
protected double mSamplesPerSymbol;
@@ -85,13 +91,13 @@ public abstract class C4FMSymbolProcessor
protected boolean mSynchronized = false;
private boolean mDelayedSyncNotificationPending = false;
// private final boolean mDebugVisualizeSync = true;
// private final boolean mDebugVisualizeSymbols = true;
// private final boolean mDebugLogDetectionScores = false;
// private final int mDebugSymbolStart = 0;
// private ISyncResultsListener mDebugSyncResultsViewer;
// private int mDebugSymbolCounter = 0;
// private int mDebugSampleCounter = 0;
private final boolean mDebugVisualizeSync = false;
private final boolean mDebugVisualizeSymbols = false;
private final boolean mDebugLogDetectionScores = false;
private final int mDebugSymbolStart = 0;
private ISyncResultsListener mDebugSyncResultsViewer;
private int mDebugSymbolCounter = 0;
private int mDebugSampleCounter = 0;
/**
* Constructs an instance
@@ -104,6 +110,7 @@ public abstract class C4FMSymbolProcessor
mFeedbackDecoder = feedbackDecoder;
mSyncDetector = primary;
mSyncDetectorLagging = lagging;
mActiveSyncDetector = primary;
mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getPatternSymbols(), getEqualizerSymbolLength(),
getEqualizerTrainingPeriodDurationSymbolCount(), getEqualizerSkipTrainingFrameCount());
mSampleEqualizer = new SampleEqualizer(isDynamicBalanceTrackingRequired());
@@ -209,6 +216,37 @@ public abstract class C4FMSymbolProcessor
*/
public abstract float getSyncThresholdEqualized();
/**
* Notification that a sync detection was validated and the correction applied. Subclasses can override this to
* capture protocol specific details (ie the detected sync pattern) from the sync detector that triggered the
* detection. The message framer is notified of the sync detection once the final sync symbol emerges from the
* equalizer delay line, which can be after this method is invoked.
* @param syncDetector that triggered the sync detection.
*/
protected void syncDetected(ISoftC4FMSyncDetector syncDetector)
{
//No-op default implementation
}
/**
* Indicates if a sync detection is allowed at the current symbol. Protocols with fixed length framing (ie DMR)
* can override this to only allow sync detections at the expected frame boundaries once synchronized, to prevent
* false sync detections from the frame payload. Default implementation always allows sync detection.
* @param symbolsSinceLastSync count of symbols since the final symbol of the previous sync detection.
* @return true if a sync detection is allowed.
*/
protected boolean isSyncDetectionAllowed(int symbolsSinceLastSync)
{
return true;
}
/**
* Number of consecutive sync periods (capped at 3) where a sync was detected within the max message length.
*/
protected int getConsecutiveSyncPeriods()
{
return mConsecutiveSyncPeriods;
}
/**
* Finalize and prepare for shutdown.
@@ -290,16 +328,16 @@ public abstract class C4FMSymbolProcessor
bufferPointer++;
samplePoint--;
// mDebugSampleCounter++;
mDebugSampleCounter++;
if(samplePoint < 1)
{
// mDebugSymbolCounter++;
//
// if(mDebugSymbolCounter % mSyncDetector.getDibitLength() == 0)
// {
// visualizeSymbols(bufferPointer, samplePoint, symbolsSinceLastSync);
// }
mDebugSymbolCounter++;
if(mDebugSymbolCounter % mSyncDetector.getDibitLength() == 0)
{
visualizeSymbols(bufferPointer, samplePoint, symbolsSinceLastSync);
}
symbolsSinceLastSync++;
@@ -337,21 +375,26 @@ public abstract class C4FMSymbolProcessor
float scoreLag = mSyncDetectorLagging.process(softSymbolLag);
scorePrimary = mSyncDetector.process(softSymbol);
// if(mDebugLogDetectionScores)
// {
// boolean exceeds = scorePrimary > 45 || scoreLag > 45;
// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] at [" + mDebugSymbolCounter + "]" + (exceeds ? " <<<<<<<<<<<<<<<<<<<<<<<<<<" : ""));
// }
if(mDebugLogDetectionScores)
{
boolean exceeds = scorePrimary > 45 || scoreLag > 45;
System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] at [" + mDebugSymbolCounter + "]" + (exceeds ? " <<<<<<<<<<<<<<<<<<<<<<<<<<" : ""));
}
correctionCandidate = INVALID_SYNC_DETECTION;
if(scorePrimary > getSyncThresholdDetected() && scorePrimary > scoreLag)
if(isSyncDetectionAllowed(symbolsSinceLastSync))
{
correctionCandidate = optimize(0, scorePrimary, (bufferPointer + samplePoint));
}
else if(scoreLag > getSyncThresholdDetected() && symbolsSinceLastSync > 1)
{
correctionCandidate = optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint));
if(scorePrimary > getSyncThresholdDetected() && scorePrimary > scoreLag)
{
mActiveSyncDetector = mSyncDetector;
correctionCandidate = optimize(0, scorePrimary, (bufferPointer + samplePoint));
}
else if(scoreLag > getSyncThresholdDetected() && symbolsSinceLastSync > 1)
{
mActiveSyncDetector = mSyncDetectorLagging;
correctionCandidate = optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint));
}
}
//Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized
@@ -361,10 +404,10 @@ public abstract class C4FMSymbolProcessor
Dibit symbol = Dibit.fromSample(softSymbol);
mFeedbackDecoder.broadcast(softSymbol);
// if(mDebugVisualizeSync)
// {
// getDebugSyncResultsViewer().symbol(softSymbol);
// }
if(mDebugVisualizeSync)
{
getDebugSyncResultsViewer().symbol(softSymbol);
}
//When the message framer emits at least one valid message (ie CRC check passes) and we have at least
// 2 consecutive sync detections, send the current sample equalizer's balance value as a pseudo-PLL
@@ -388,12 +431,13 @@ public abstract class C4FMSymbolProcessor
{
mConsecutiveSyncPeriods = Math.min(3, mConsecutiveSyncPeriods + 1);
// if(mDebugLogDetectionScores)
// {
// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] Optimized [" + correctionCandidate.mOptimizationScore + "] at samples [" + mDebugSampleCounter + "] symbols [" + mDebugSymbolCounter + "]");
// }
if(mDebugLogDetectionScores)
{
System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] Optimized [" + correctionCandidate.mOptimizationScore + "] at samples [" + mDebugSampleCounter + "] symbols [" + mDebugSymbolCounter + "]");
}
mSampleEqualizer.apply(correctionCandidate);
syncDetected(mActiveSyncDetector);
if(mSynchronized)
{
@@ -424,7 +468,7 @@ public abstract class C4FMSymbolProcessor
// need to prime that buffer with aligned samples prior to sync detection training.
if(!mSymbolEqualizer.isEnabled())
{
int symbols = mSyncDetector.getPatternSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount();
int symbols = mSyncDetector.getDibitLength() + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount();
double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset;
float mid, sym;
@@ -440,9 +484,9 @@ public abstract class C4FMSymbolProcessor
}
}
mSymbolEqualizer.syncDetected();
mSymbolEqualizer.syncDetected(mActiveSyncDetector.getPatternSymbols());
mSymbolEqualizer.enable();
// visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint);
visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint);
}
else if(mDelayedSyncNotificationPending)
{
@@ -485,26 +529,27 @@ public abstract class C4FMSymbolProcessor
*/
private Correction validateSyncWaveform(Correction candidate, int bufferPointer, double samplePoint)
{
ReferenceSyncWaveform referenceSyncWaveform = mActiveSyncDetector.getReferenceWaveform(mSamplesPerSymbol);
float gain = mSampleEqualizer.mGain + candidate.getGainAdjustment();
float balance = mSampleEqualizer.mBalance + candidate.getBalanceAdjustment();
double offset = bufferPointer + samplePoint + candidate.getTimingCorrection() - mReferenceSyncWaveform.samples().length;
double offset = bufferPointer + samplePoint + candidate.getTimingCorrection() - referenceSyncWaveform.samples().length;
int integral = (int)Math.ceil(offset);
double mu = integral - offset;
double accumulator = 0.0;
int index;
//Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample
for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--)
for(int x = referenceSyncWaveform.samples().length - 1; x >= 0; x--)
{
index = integral + x;
float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu);
sample *= gain;
sample += balance;
sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample());
accumulator += mReferenceSyncWaveform.samples()[x] * sample;
sample = Math.clamp(sample, -referenceSyncWaveform.maxSample(), referenceSyncWaveform.maxSample());
accumulator += referenceSyncWaveform.samples()[x] * sample;
}
double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
double threshold = referenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
//Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static
// invalid sync detection to nullify the correction.
@@ -548,156 +593,157 @@ public abstract class C4FMSymbolProcessor
mSyncWorkspaceLength = (int)Math.ceil((mSyncDetector.getDibitLength() + 1) * samplesPerSymbol);
mBufferReloadLength = (int)Math.ceil(samplesPerSymbol);
mSampleEqualizer.configure(samplesPerSymbol);
mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol);
//Pre-generate the reference sync waveform for the primary sync pattern
mSyncDetector.getReferenceWaveform(samplesPerSymbol);
// System.out.println("Samples Per Symbol: " + samplesPerSymbol);
}
// /**
// * Debug viewer UI for visualizing sync detections in the sample stream/waveform.
// */
// private ISyncResultsListener getDebugSyncResultsViewer()
// {
// if(mDebugSyncResultsViewer == null)
// {
// mDebugSyncResultsViewer = new FMSyncResultsViewer();
// }
//
// return mDebugSyncResultsViewer;
// }
//
// /**
// * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
// * @param score from the sync detector
// * @param primary sync detector (true) or secondary (false)
// */
// public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
// {
// if(!mDebugVisualizeSync || mDebugSymbolCounter < mDebugSymbolStart)
// {
// return;
// }
//
// int integral = (int)Math.floor(samplePoint);
// samplePoint -= integral;
// bufferPointer += integral;
//
// int offset = 3;
// int length = (int)Math.ceil(mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)) + (2 * offset);
// int end = bufferPointer + offset;
// int start = end - length;
// float[] symbols = new float[mSyncDetector.getDibitLength()];
// float[] samples = Arrays.copyOfRange(mBuffer, start, end);
// for(int i = 0; i < samples.length; i++)
// {
// samples[i] = mSampleEqualizer.equalize(samples[i]);
// }
//
// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
// int symbolIntegral = (int)Math.floor(symbolPointer);
// double mu = symbolPointer - symbolIntegral;
//
// for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
// {
// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
// symbolPointer += mSamplesPerSymbol;
// symbolIntegral = (int)Math.floor(symbolPointer);
// mu = symbolPointer - symbolIntegral;
// }
//
// float[] syncIntervals = new float[mSyncDetector.getDibitLength()];
// int adjust = bufferPointer - length + offset;
// double pointer = bufferPointer + samplePoint - adjust;
//
// for(int x = (mSyncDetector.getDibitLength() - 1); x >= 0; x--)
// {
// syncIntervals[x] = (float)pointer;
// pointer -= mSamplesPerSymbol;
// }
//
// CountDownLatch countDownLatch = new CountDownLatch(1);
// getDebugSyncResultsViewer().receive(symbols, mSyncDetector.getPatternSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
// tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") +
// " EQ-BO:" + DEBUG_DF.format(mSampleEqualizer.mBalanceOptimized) +
// " EQ-B:" + DEBUG_DF.format(mSampleEqualizer.mBalance) +
// " EQ-G:" + DEBUG_DF.format(mSampleEqualizer.mGain) + " " +
// " SAM:" + mDebugSampleCounter +
// " SYM:" + mDebugSymbolCounter +
// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0),
// countDownLatch);
//
// try
// {
// countDownLatch.await();
// }
// catch(InterruptedException e)
// {
// throw new RuntimeException(e);
// }
// }
//
// /**
// * Debug method to visualize the symbol decisions
// */
// public void visualizeSymbols(int bufferPointer, double samplePoint, int symbolsSinceLastSync)
// {
// if(!mDebugVisualizeSymbols || mDebugSymbolCounter < mDebugSymbolStart)
// {
// return;
// }
//
// int syncLength = mSyncDetector.getDibitLength();
//
// int offset = 3;
// int length = (int)Math.ceil(mSamplesPerSymbol * (syncLength - 1)) + (2 * offset);
// int end = bufferPointer + offset;
// int start = end - length;
// float[] symbols = new float[syncLength];
// float[] decisions = new float[syncLength];
// float[] samples = Arrays.copyOfRange(mBuffer, start, end);
// for(int i = 0; i < samples.length; i++)
// {
// samples[i] = mSampleEqualizer.equalize(samples[i]);
// }
//
// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (syncLength - 1));
// int symbolIntegral = (int)Math.floor(symbolPointer);
// double mu = symbolPointer - symbolIntegral;
//
// for(int x = 0; x < syncLength; x++)
// {
// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
// decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase();
// symbolPointer += mSamplesPerSymbol;
// symbolIntegral = (int)Math.floor(symbolPointer);
// mu = symbolPointer - symbolIntegral;
// }
//
// float[] syncIntervals = new float[syncLength];
// int adjust = bufferPointer - length + offset;
// double pointer = bufferPointer + samplePoint - adjust;
//
// for(int x = syncLength - 1; x >= 0; x--)
// {
// syncIntervals[x] = (float)pointer;
// pointer -= mSamplesPerSymbol;
// }
//
// CountDownLatch countDownLatch = new CountDownLatch(1);
// getDebugSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
// "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain +
// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter +
// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0) +
// " ELAPSED:" + symbolsSinceLastSync, countDownLatch);
//
// try
// {
// countDownLatch.await();
// }
// catch(InterruptedException e)
// {
// throw new RuntimeException(e);
// }
// }
/**
* Debug viewer UI for visualizing sync detections in the sample stream/waveform.
*/
private ISyncResultsListener getDebugSyncResultsViewer()
{
if(mDebugSyncResultsViewer == null)
{
mDebugSyncResultsViewer = new FMSyncResultsViewer();
}
return mDebugSyncResultsViewer;
}
/**
* Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
* @param score from the sync detector
* @param primary sync detector (true) or secondary (false)
*/
public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
{
if(!mDebugVisualizeSync || mDebugSymbolCounter < mDebugSymbolStart)
{
return;
}
int integral = (int)Math.floor(samplePoint);
samplePoint -= integral;
bufferPointer += integral;
int offset = 3;
int length = (int)Math.ceil(mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)) + (2 * offset);
int end = bufferPointer + offset;
int start = end - length;
float[] symbols = new float[mSyncDetector.getDibitLength()];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
for(int i = 0; i < samples.length; i++)
{
samples[i] = mSampleEqualizer.equalize(samples[i]);
}
double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
{
symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
symbolPointer += mSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
float[] syncIntervals = new float[mSyncDetector.getDibitLength()];
int adjust = bufferPointer - length + offset;
double pointer = bufferPointer + samplePoint - adjust;
for(int x = (mSyncDetector.getDibitLength() - 1); x >= 0; x--)
{
syncIntervals[x] = (float)pointer;
pointer -= mSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
getDebugSyncResultsViewer().receive(symbols, mSyncDetector.getPatternSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") +
" EQ-BO:" + DEBUG_DF.format(mSampleEqualizer.mBalanceOptimized) +
" EQ-B:" + DEBUG_DF.format(mSampleEqualizer.mBalance) +
" EQ-G:" + DEBUG_DF.format(mSampleEqualizer.mGain) + " " +
" SAM:" + mDebugSampleCounter +
" SYM:" + mDebugSymbolCounter +
" DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0),
countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Debug method to visualize the symbol decisions
*/
public void visualizeSymbols(int bufferPointer, double samplePoint, int symbolsSinceLastSync)
{
if(!mDebugVisualizeSymbols || mDebugSymbolCounter < mDebugSymbolStart)
{
return;
}
int syncLength = mSyncDetector.getDibitLength();
int offset = 3;
int length = (int)Math.ceil(mSamplesPerSymbol * (syncLength - 1)) + (2 * offset);
int end = bufferPointer + offset;
int start = end - length;
float[] symbols = new float[syncLength];
float[] decisions = new float[syncLength];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
for(int i = 0; i < samples.length; i++)
{
samples[i] = mSampleEqualizer.equalize(samples[i]);
}
double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (syncLength - 1));
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < syncLength; x++)
{
symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase();
symbolPointer += mSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
float[] syncIntervals = new float[syncLength];
int adjust = bufferPointer - length + offset;
double pointer = bufferPointer + samplePoint - adjust;
for(int x = syncLength - 1; x >= 0; x--)
{
syncIntervals[x] = (float)pointer;
pointer -= mSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
getDebugSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
"Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain +
" SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter +
" DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0) +
" ELAPSED:" + symbolsSinceLastSync, countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Calculates the standard deviation of the samples at the specified offset that aligns with a sync detection.
@@ -965,7 +1011,10 @@ public abstract class C4FMSymbolProcessor
/**
* Applies decision directed correction to the constellation balance when dynamic balance tracking is enabled
* and the processor is synchronized following a valid sync detection.
* and the processor is synchronized following a valid sync detection. In periods of high noise or when the
* signal has stopped but we haven't yet detected it, we apply a correction that gradually restores the balance
* back towards the optimized balance decided at the last valid sync detection. This ensures that when the
* signal returns, the balance can be reestablished quickly.
* @param symbol sliced at the correct timing
*/
public float correct(float symbol)
@@ -1153,8 +1202,9 @@ public abstract class C4FMSymbolProcessor
{
int maxPointer = mBuffer.length - 1;
float softSymbol;
float[] patternSymbols = mActiveSyncDetector.getPatternSymbols();
double pointer = offset - (samplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
double pointer = offset - (samplesPerSymbol * (patternSymbols.length - 1));
int bufferPointer = (int)Math.floor(pointer);
double fractional = pointer - bufferPointer;
@@ -1162,7 +1212,7 @@ public abstract class C4FMSymbolProcessor
float sample1, sample2;
float idealSymbol;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
for(int x = 0; x < patternSymbols.length; x++)
{
if(bufferPointer < maxPointer)
{
@@ -1175,7 +1225,7 @@ public abstract class C4FMSymbolProcessor
softSymbol = 0.0f;
}
idealSymbol = mSyncDetector.getPatternSymbols()[x];
idealSymbol = patternSymbols[x];
//Constrain the contribution of the soft symbol to +/- the ideal symbol value.
if(idealSymbol > 0)
@@ -1246,22 +1296,24 @@ public abstract class C4FMSymbolProcessor
public Correction getCorrection(double additionalOffset, double timingCorrection,
float detectionScore, float optimizationScore, double offset, float measuredBalance)
{
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
float[] patternSymbols = mActiveSyncDetector.getPatternSymbols();
Dibit[] patternDibits = mActiveSyncDetector.getPatternDibits();
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (patternSymbols.length - 1));
int resampleStartIntegral = (int)Math.floor(resampleStart);
float symbol, gainAccumulator = 0, idealMagnitudeAccumulator = 0, resampledSoftSymbol;
Dibit resampledDibit;
int bitErrorCount = 0;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
for(int x = 0; x < patternSymbols.length; x++)
{
if(resampleStartIntegral >= 0)
{
symbol = mSyncDetector.getPatternSymbols()[x];
symbol = patternSymbols[x];
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
resampledSoftSymbol = resampledSoftSymbol * mSampleEqualizer.mGain + measuredBalance;
resampledDibit = Dibit.fromSample(resampledSoftSymbol);
bitErrorCount += mSyncDetector.getPatternDibits()[x].getBitErrorFrom(resampledDibit);
bitErrorCount += patternDibits[x].getBitErrorFrom(resampledDibit);
gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol);
idealMagnitudeAccumulator += Math.abs(symbol);
}

View File

@@ -15,13 +15,14 @@ public interface ISoftC4FMSyncDetector
float process(float softSymbol);
/**
* Soft symbols that represent the sync pattern
* Soft symbols that represent the sync pattern. For detectors that monitor multiple sync patterns (ie DMR), this
* is the sync pattern that produced the score from the most recent call to process().
* @return soft symbols array
*/
float[] getPatternSymbols();
/**
* Array of dibits for the sync pattern
* Array of dibits for the sync pattern (or most recently detected sync pattern for multi-pattern detectors).
* @return sync pattern dibit array
*/
Dibit[] getPatternDibits();
@@ -32,7 +33,8 @@ public interface ISoftC4FMSyncDetector
int getDibitLength();
/**
* Provides a reference waveform that represents the sync pattern for sync detection validation
* Provides a reference waveform that represents the sync pattern (or most recently detected sync pattern for
* multi-pattern detectors) for sync detection validation
* @param samplesPerSymbol for each symbol
* @return reference waveform
*/

View File

@@ -0,0 +1,288 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.edac.soft;
import java.util.Arrays;
import java.util.function.IntPredicate;
/**
* Soft decision decoder for short binary block codes, defined by the full set of valid codewords.
*
* Codewords are represented as integers where bit i (LSB = 0) holds codeword bit position i. Soft bit values are
* log-likelihood ratios where a positive value favors a 1 bit and the magnitude indicates the reliability.
*
* Provides Chase-Pyndiah soft-in/soft-out decoding: tests the codewords nearest to the hard decision with the least
* reliable bits flipped and produces extrinsic soft information for iterative (ie product code) decoding.
*/
public class SoftBlockCode
{
private static final int MAX_LENGTH = 17;
private final int mLength;
private final int[] mCodewords;
private final int[] mDecodeTable;
/**
* Constructs an instance.
* @param length of the code in bits (maximum 17).
* @param codewords set of all valid codewords.
*/
public SoftBlockCode(int length, int[] codewords)
{
if(length < 1 || length > MAX_LENGTH)
{
throw new IllegalArgumentException("Code length must be in range 1 - " + MAX_LENGTH);
}
mLength = length;
mCodewords = codewords.clone();
//Single bit error correction table: maps each word within distance 1 of a codeword to that codeword, or -1.
mDecodeTable = new int[1 << length];
Arrays.fill(mDecodeTable, -1);
for(int codeword: codewords)
{
mDecodeTable[codeword] = codeword;
for(int i = 0; i < length; i++)
{
mDecodeTable[codeword ^ (1 << i)] = codeword;
}
}
}
/**
* Creates an instance by testing every possible word of the specified length to identify the valid codewords.
* @param length of the code in bits (maximum 17).
* @param isCodeword test that indicates if the word (bit i = codeword position i) is a valid codeword.
* @return soft block code.
*/
public static SoftBlockCode fromCodewordTest(int length, IntPredicate isCodeword)
{
int count = 0;
int[] candidates = new int[1 << length];
for(int word = 0; word < (1 << length); word++)
{
if(isCodeword.test(word))
{
candidates[count++] = word;
}
}
return new SoftBlockCode(length, Arrays.copyOf(candidates, count));
}
/**
* Code length in bits
*/
public int getLength()
{
return mLength;
}
/**
* Valid codewords
*/
public int[] getCodewords()
{
return mCodewords;
}
/**
* Indicates if the word is a valid codeword.
*/
public boolean isCodeword(int word)
{
return mDecodeTable[word] == word;
}
/**
* Hard decision of the soft bits.
* @param softBits array
* @param indices of the code bits in the soft bits array
* @return hard decision word.
*/
public static int getHardDecision(float[] softBits, int[] indices)
{
int word = 0;
for(int i = 0; i < indices.length; i++)
{
if(softBits[indices[i]] > 0)
{
word |= (1 << i);
}
}
return word;
}
/**
* Calculates the reliability metric for the candidate codeword: the sum of the soft bit magnitudes for each bit
* where the candidate differs from the hard decision. Lower values are more likely.
*/
private static float getMetric(int candidate, int hard, float[] magnitudes)
{
float metric = 0.0f;
int difference = candidate ^ hard;
while(difference != 0)
{
int bit = Integer.numberOfTrailingZeros(difference);
metric += magnitudes[bit];
difference &= difference - 1;
}
return metric;
}
/**
* Chase-Pyndiah soft-in/soft-out decode. Generates test patterns by flipping each combination of the least
* reliable bits in the hard decision, decodes each with the single bit error correction table and selects the
* most likely candidate codeword as the decision. For each bit, the extrinsic value is the metric difference
* between the decision and the best competing candidate that differs at that bit, less the soft input, or beta
* (signed by the decision) when there is no competing candidate. When no test pattern decodes to a codeword, the
* extrinsic values are zero.
*
* @param input soft bits array
* @param indices of the code bits in the input and output arrays
* @param output array to receive the extrinsic soft values at the code bit indices
* @param leastReliableCount number of least reliable bits to use for test patterns
* @param beta reliability to assign to a bit when there is no competing candidate codeword.
* @return decision codeword, or -1 if no test pattern decoded to a codeword.
*/
public int decodeChase(float[] input, int[] indices, float[] output, int leastReliableCount, float beta)
{
float[] magnitudes = new float[mLength];
for(int i = 0; i < mLength; i++)
{
magnitudes[i] = Math.abs(input[indices[i]]);
}
int hard = getHardDecision(input, indices);
//Identify the least reliable bit positions using selection sort over the small code length
int p = Math.min(leastReliableCount, mLength);
int[] leastReliable = new int[p];
int used = 0;
for(int x = 0; x < p; x++)
{
int minIndex = -1;
for(int i = 0; i < mLength; i++)
{
if((used & (1 << i)) == 0 && (minIndex < 0 || magnitudes[i] < magnitudes[minIndex]))
{
minIndex = i;
}
}
leastReliable[x] = minIndex;
used |= (1 << minIndex);
}
//Decode each test pattern into a unique candidate codeword
int[] candidates = new int[1 << p];
float[] metrics = new float[1 << p];
int candidateCount = 0;
int decision = -1;
float decisionMetric = Float.MAX_VALUE;
for(int pattern = 0; pattern < (1 << p); pattern++)
{
int test = hard;
for(int b = 0; b < p; b++)
{
if((pattern & (1 << b)) != 0)
{
test ^= (1 << leastReliable[b]);
}
}
int candidate = mDecodeTable[test];
if(candidate < 0)
{
continue;
}
boolean duplicate = false;
for(int c = 0; c < candidateCount; c++)
{
if(candidates[c] == candidate)
{
duplicate = true;
break;
}
}
if(!duplicate)
{
float metric = getMetric(candidate, hard, magnitudes);
candidates[candidateCount] = candidate;
metrics[candidateCount] = metric;
candidateCount++;
if(metric < decisionMetric)
{
decisionMetric = metric;
decision = candidate;
}
}
}
for(int i = 0; i < mLength; i++)
{
if(decision < 0)
{
output[indices[i]] = 0.0f;
continue;
}
int mask = 1 << i;
float sign = (decision & mask) != 0 ? 1.0f : -1.0f;
float competitorMetric = Float.MAX_VALUE;
for(int c = 0; c < candidateCount; c++)
{
if(((candidates[c] ^ decision) & mask) != 0 && metrics[c] < competitorMetric)
{
competitorMetric = metrics[c];
}
}
if(competitorMetric < Float.MAX_VALUE)
{
output[indices[i]] = sign * (competitorMetric - decisionMetric) - input[indices[i]];
}
else
{
output[indices[i]] = sign * beta;
}
}
return decision;
}
}

View File

@@ -0,0 +1,278 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.edac.soft;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Iterative soft decision decoder for product codes where each row is a codeword of a short block code and each
* column has even parity (ie the final row is the column parity row). Bits are ordered row by row.
*
* Alternately decodes the rows using Chase-Pyndiah decoding and the even parity columns using min-sum decoding,
* exchanging extrinsic soft information, until the decisions form a valid product codeword. Rejects decisions
* where the correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) exceeds the
* maximum, since a low correction cost indicates a reliable decision.
*/
public class SoftProductCodeDecoder
{
private static final int CHASE_LEAST_RELIABLE_BITS = 4;
//Each half iteration decodes either all rows or all columns, starting with the rows. Alpha scales the extrinsic
//information fed to the next half iteration and beta is the reliability assigned to row bits without a competing
//candidate codeword.
private static final float[] SOFT_ALPHA = new float[]{0.0f, 0.2f, 0.3f, 0.5f, 0.7f, 0.9f, 1.0f, 1.0f};
private static final float[] SOFT_BETA = new float[]{0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f};
private final SoftBlockCode mRowCode;
private final int mRowCount;
private final int mColumnCount;
private final int mLength;
private final int[][] mRowIndexes;
private final float mMaxCorrectionCost;
/**
* Constructs an instance
* @param rowCode for each row, where the code length is the column count
* @param rowCount number of rows, including the column parity row
* @param maxCorrectionCost maximum correction cost for a decision, or Float.MAX_VALUE for no limit
*/
public SoftProductCodeDecoder(SoftBlockCode rowCode, int rowCount, float maxCorrectionCost)
{
mRowCode = rowCode;
mRowCount = rowCount;
mColumnCount = rowCode.getLength();
mLength = mRowCount * mColumnCount;
mMaxCorrectionCost = maxCorrectionCost;
mRowIndexes = new int[mRowCount][mColumnCount];
for(int row = 0; row < mRowCount; row++)
{
for(int column = 0; column < mColumnCount; column++)
{
mRowIndexes[row][column] = row * mColumnCount + column;
}
}
}
/**
* Decodes the (de-interleaved) soft bits.
* @param channel soft bits ordered row by row where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge or the correction
* cost exceeds the maximum.
*/
public CorrectedBinaryMessage decode(float[] channel)
{
if(channel == null || channel.length != mLength)
{
return null;
}
//Normalize the channel soft bits to a mean magnitude of 1 for the alpha and beta settings
float sum = 0.0f;
for(float softBit: channel)
{
sum += Math.abs(softBit);
}
if(sum <= 0.0f)
{
return null;
}
float scale = mLength / sum;
float[] received = new float[mLength];
for(int i = 0; i < mLength; i++)
{
received[i] = channel[i] * scale;
}
float[] extrinsic = new float[mLength];
float[] input = new float[mLength];
float[] output = new float[mLength];
for(int halfIteration = 0; halfIteration < SOFT_ALPHA.length; halfIteration++)
{
float alpha = SOFT_ALPHA[halfIteration];
for(int i = 0; i < mLength; i++)
{
input[i] = received[i] + alpha * extrinsic[i];
}
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(mLength);
if(halfIteration % 2 == 0)
{
boolean allDecoded = true;
for(int row = 0; row < mRowCount; row++)
{
int word = mRowCode.decodeChase(input, mRowIndexes[row], output, CHASE_LEAST_RELIABLE_BITS,
SOFT_BETA[halfIteration]);
allDecoded &= word >= 0;
for(int column = 0; column < mColumnCount; column++)
{
if((word & (1 << column)) != 0)
{
decoded.set(mRowIndexes[row][column]);
}
}
}
if(!allDecoded)
{
decoded = null;
}
}
else
{
decodeColumns(input, output);
for(int i = 0; i < mLength; i++)
{
if(input[i] + output[i] > 0)
{
decoded.set(i);
}
}
}
//Check for a valid product codeword from the decisions of this half iteration
if(decoded != null && isValid(decoded))
{
int corrected = 0;
float cost = 0.0f;
for(int i = 0; i < mLength; i++)
{
if(decoded.get(i) != (received[i] > 0))
{
corrected++;
cost += Math.abs(received[i]);
}
}
if(cost > mMaxCorrectionCost)
{
return null;
}
decoded.setCorrectedBitCount(corrected);
return decoded;
}
//Normalize the extrinsic information to a mean magnitude of 1 for the next half iteration
float extrinsicSum = 0.0f;
for(float value: output)
{
extrinsicSum += Math.abs(value);
}
float extrinsicScale = extrinsicSum > 0.0f ? mLength / extrinsicSum : 0.0f;
for(int i = 0; i < mLength; i++)
{
extrinsic[i] = output[i] * extrinsicScale;
}
}
return null;
}
/**
* Indicates if each row is a row codeword and each column has even parity.
*/
private boolean isValid(CorrectedBinaryMessage message)
{
int parity = 0;
for(int row = 0; row < mRowCount; row++)
{
int word = 0;
for(int column = 0; column < mColumnCount; column++)
{
if(message.get(mRowIndexes[row][column]))
{
word |= (1 << column);
}
}
if(!mRowCode.isCodeword(word))
{
return false;
}
parity ^= word;
}
return parity == 0;
}
/**
* Min-sum (max-log) soft-in/soft-out decoding of the even parity columns. The extrinsic value for each bit is
* the product of the signs and the minimum magnitude of the other bits in the column.
* @param input soft bits
* @param output to receive the extrinsic values.
*/
private void decodeColumns(float[] input, float[] output)
{
for(int column = 0; column < mColumnCount; column++)
{
float sign = 1.0f;
float minimum1 = Float.MAX_VALUE;
float minimum2 = Float.MAX_VALUE;
int minimumRow = -1;
for(int row = 0; row < mRowCount; row++)
{
float value = input[mRowIndexes[row][column]];
if(value < 0)
{
sign = -sign;
}
float magnitude = Math.abs(value);
if(magnitude < minimum1)
{
minimum2 = minimum1;
minimum1 = magnitude;
minimumRow = row;
}
else if(magnitude < minimum2)
{
minimum2 = magnitude;
}
}
for(int row = 0; row < mRowCount; row++)
{
int index = mRowIndexes[row][column];
float value = input[index];
float otherSign = value < 0 ? -sign : sign;
output[index] = otherSign * (row == minimumRow ? minimum2 : minimum1);
}
}
}
}

View File

@@ -26,8 +26,8 @@ import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
import io.github.dsheirer.dsp.fm.FmDemodulatorFactory;
import io.github.dsheirer.dsp.fm.IDemodulator;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.DecoderType;
@@ -40,6 +40,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.nxdn.NXDNFilterDesigner;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
@@ -52,25 +53,24 @@ import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DMR DQPSK decoder.
* DMR 4FSK decoder.
*
* Processes the sample stream using an optimal (scalar or vector) DQPSK demodulator. The demodulated sample stream
* is processed by the DMRSoftSymbolProcessor at each symbol time interval using a primary and two lagging soft sync
* detectors. The sync detectors use correlation of the demodulated soft symbols with all known sync patterns to
* detect the sync and align the symbol timing and symbol period to the optimal location and symbol period as
* calculated across each detected burst. Once the symbol processor has fingerprinted the channel type, it optimizes
* sync detection for one of three detected modes (BASE, MOBILE or DIRECT) to reduce sync correlation workload.
* Processes the sample stream using an FM demodulator and a root raised cosine receive filter matched to the DMR
* transmit filter. The demodulated sample stream is processed by the DMRSymbolProcessor (C4FM symbol processor)
* at each symbol time interval using a primary and a lagging soft sync detector. The sync detectors use correlation
* of the demodulated soft symbols with all known sync patterns to detect the sync and align the symbol timing and
* constellation gain and balance. Once the symbol processor has fingerprinted the channel type, it optimizes sync
* detection for one of three detected modes (BASE, MOBILE or DIRECT) to reduce sync correlation workload.
*
* The DMRSoftSymbolMessageFramer receives sync detections from the soft symbol processor and buffers symbols from the
* start of the burst through the sync so that the symbols can be resampled when the sync is detected, ensuring that
* the burst symbols are accurate using the symbol timing and period values calculated on the sync detection.
* The DMRMessageFramer receives sync detections from the soft symbol processor and buffers symbols from the start of
* the burst through the sync so that the full burst can be framed once the sync is detected.
*
* For Direct Mode where one timeslot is empty, the symbol message framer uses two message burst buffers that alternate
* for each burst so that one buffer tracks the active timeslot and the other buffer tracks the empty timeslot to
@@ -85,18 +85,19 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
private static final int SYMBOL_RATE = 4800;
private static final float MAXIMUM_CARRIER_OFFSET = 5000.0f; //Threshold for retuning the signal.
private static final Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
private DifferentialDemodulatorFloat mDemodulator;
//DMR transmit filter is a root raised cosine with 0.2 roll-off, so the receive filter is a matched RRC filter.
private static final double RECEIVE_FILTER_ALPHA = 0.2;
private static final int RECEIVE_FILTER_SYMBOL_SPAN = 12;
private static final Map<Double,float[]> BASEBAND_FILTERS = new ConcurrentHashMap<>();
private IDemodulator mDemodulator;
private final DMRMessageFramer mMessageFramer;
private final DMRSoftSymbolProcessor mSymbolProcessor;
private final DMRSymbolProcessor mSymbolProcessor;
private final DMRMessageProcessor mMessageProcessor;
private IRealFilter mIBasebandFilter;
private IRealFilter mQBasebandFilter;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private RealFIRFilter mRRCFilterI;
private RealFIRFilter mRRCFilterQ;
private IRealFilter mReceiveFilter;
/**
* Constructs an instance
@@ -106,7 +107,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
DMRCrcMaskManager crcMaskManager = new DMRCrcMaskManager(config.getIgnoreCRCChecksums());
mMessageFramer = new DMRMessageFramer(crcMaskManager);
mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer, this);
mSymbolProcessor = new DMRSymbolProcessor(this, mMessageFramer);
mMessageProcessor = new DMRMessageProcessor(config, crcMaskManager);
mMessageProcessor.setMessageListener(getMessageListener());
setSampleRate(25000.0);
@@ -129,21 +130,20 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
*/
public void setSampleRate(double sampleRate)
{
if(sampleRate <= SYMBOL_RATE * 2)
{
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
SYMBOL_RATE + " symbol rate)");
}
float samplesPerSymbol = (float)sampleRate / SYMBOL_RATE;
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
if(samplesPerSymbol <= 4.0f)
{
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be at least " + (SYMBOL_RATE * 4) + " Hz");
}
int decimation = 1;
//Identify decimation that gets us as close to 4.0 Samples Per Symbol as possible (19.2 kHz)
while((sampleRate / decimation) >= 38400)
//Identify decimation that gets us as close to 5.0 Samples Per Symbol as possible
while(samplesPerSymbol >= 10.0)
{
decimation *= 2;
samplesPerSymbol /= 2;
}
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
@@ -154,21 +154,21 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
//Set the decimated sample rate to use for PLL error reporting.
setDecimatedSampleRate(decimatedSampleRate);
float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate));
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
symbolLength += symbolLength % 2; //Make the symbol length even
if(symbolLength < 0)
//Receive filter: root raised cosine matched to the DMR transmit filter, normalized to unity gain at DC.
double[] receiveFilterTaps = NXDNFilterDesigner.designRootRaisedCosine(decimatedSampleRate, SYMBOL_RATE,
RECEIVE_FILTER_ALPHA, RECEIVE_FILTER_SYMBOL_SPAN);
float[] taps = new float[receiveFilterTaps.length];
for(int x = 0; x < receiveFilterTaps.length; x++)
{
symbolLength = 2;
taps[x] = (float)receiveFilterTaps[x];
}
mReceiveFilter = new RealFIRFilter(taps);
float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / SYMBOL_RATE, symbolLength, rrcAlpha);
mRRCFilterI = new RealFIRFilter(taps);
mRRCFilterQ = new RealFIRFilter(taps);
mBasebandFilterI = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mBasebandFilterQ = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate));
mDemodulator = DifferentialDemodulatorFactory.getFloatDemodulator(decimatedSampleRate, SYMBOL_RATE);
mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol());
mDemodulator = FmDemodulatorFactory.getFmDemodulator();
mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol);
mMessageFramer.setListener(mMessageProcessor);
mMessageProcessor.setMessageListener(getMessageListener());
}
@@ -181,19 +181,20 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
public void receive(ComplexSamples samples)
{
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
float[] i = mDecimationFilterI.decimateReal(samples.i());
float[] q = mDecimationFilterQ.decimateReal(samples.q());
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
i = mRRCFilterI.filter(i);
q = mRRCFilterQ.filter(q);
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
float[] demodulated = mDemodulator.demodulate(i, q);
mSymbolProcessor.receive(demodulated);
demodulated = mReceiveFilter.filter(demodulated);
//Process demodulated samples into symbols and apply message sync detection and framing.
mSymbolProcessor.process(demodulated);
}
/**
@@ -201,9 +202,12 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
*/
private float[] getBasebandFilter(double sampleRate)
{
if(BASEBAND_FILTERS.containsKey(sampleRate))
//Filter cache is shared across decoder instances that may be started from different threads
float[] cached = BASEBAND_FILTERS.get(sampleRate);
if(cached != null)
{
return BASEBAND_FILTERS.get(sampleRate);
return cached;
}
FIRFilterSpecification specification = FIRFilterSpecification
@@ -219,11 +223,15 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
try
{
coefficients = FilterFactory.getTaps(specification);
BASEBAND_FILTERS.put(sampleRate, coefficients);
if(coefficients != null)
{
BASEBAND_FILTERS.put(sampleRate, coefficients);
}
}
catch(Exception fde) //FilterDesignException
catch(Exception e) //FilterDesignException or runtime failures from the Remez designer
{
System.out.println("Error");
LOGGER.error("Error designing DMR baseband filter for sample rate [" + sampleRate + "]", e);
}
if(coefficients == null)
@@ -270,7 +278,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
@Override
public String getProtocolDescription()
{
return "DMR C4FM";
return "DMR";
}
@Override
@@ -285,6 +293,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
super.stop();
mMessageFramer.stop();
mSymbolProcessor.dispose();
}
/**
@@ -296,6 +305,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
mSymbolProcessor.resetBalance();
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
@@ -314,8 +327,8 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
LOGGER.info("Starting ...");
// String directory = "D:\\DQPSK Equalizer Research\\"; //Windows
String directory = "/media/denny/T9/DQPSK Equalizer Research/"; //Linux
String file = directory + "DMR_1_CAPPLUS.wav";
String directory = "/run/media/denny/T9/DQPSK Equalizer Research/"; //Linux
// String file = directory + "DMR_1_CAPPLUS.wav";
// String file = directory + "DMR_2_CAPPLUS.wav";
// String file = directory + "DMR_3_CAPPLUS.wav";
// String file = directory + "20230819_064211_451250000_SaiaNet_Syracuse_Control_29_baseband.wav";
@@ -325,7 +338,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
// String file = directory + "DMR_6_20241217_031511_451425000_SaiaNet_Onondaga_SaiaNet_Control_50_baseband.wav";
// String file = directory + "DMR_7_20241217_031651_451250000_SaiaNet_Onondaga_SaiaNet_Control_50_baseband.wav";
// String file = directory + "DMR_8_20241217_031845_461662500_SaiaNet_(Tier_III)_Onondaga_Control_25_baseband.wav";
// String file = directory + "DMR_9_CAPPLUS_encrypted_American_Airlines_Maricopa_Control_29_baseband.wav";
String file = directory + "DMR_9_CAPPLUS_encrypted_American_Airlines_Maricopa_Control_29_baseband.wav";
// String file = directory + "DMR_10_CAP_ENCRYPTED_20241222_035408_935487500_American_Airlines_Maricopa_Control_1_baseband.wav";
// String file = directory + "DMR_12_20250420_061639_451250000_SaiaNet_Onondaga_SaiaNet-Control_1_baseband.wav";
// String file = directory + "DMR_17_20250516_053150_451425000_SaiaNet_Onondaga_SaiaNet-Control_1_baseband.wav";

View File

@@ -20,7 +20,9 @@
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitDelayLine;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.dmr.message.CACH;
@@ -38,14 +40,34 @@ import java.util.Arrays;
* <p>
* This framer is able to detect DMR Direct mode and track one active and one inactive timeslot across each voice
* super frame in addition to framing base and mobile station data and voice bursts.
* <p>
* Supports two symbol stream inputs:
* <ul>
* <li>Listener receive(Dibit) and syncDetected(DMRSyncPattern) where the sync detection is signalled when the
* next dibit is the first dibit of the burst (ie the symbol processor maintains a burst delay line)</li>
* <li>C4FM message framer process(Dibit) and syncDetected() where the sync detection is signalled when the most
* recent dibit is the final dibit of the sync pattern. This framer maintains a delay line to retain the burst
* dibits from CACH through the sync pattern and the sync pattern must be provided via setSyncPattern() prior to
* the sync detection notification.</li>
* </ul>
*/
public class DMRMessageFramer implements Listener<Dibit>
public class DMRMessageFramer extends C4FMMessageFramer implements Listener<Dibit>
{
private static final int DIBIT_CACH_START = 0;
private static final int DIBIT_BURST_END = 144;
//Dibit delay line sizing: CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
private static final int DELAY_LINE_LENGTH = 90;
private final DibitDelayLine mDibitDelayLine = new DibitDelayLine(DELAY_LINE_LENGTH);
//Soft symbols (radians) delay line parallel to the dibit delay line, or NaN when the soft symbol wasn't provided
private final float[] mSoftSymbolDelayLine = new float[DELAY_LINE_LENGTH];
private int mSoftSymbolDelayLinePointer = 0;
private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
private Listener<IMessage> mMessageListener;
private final Dibit[] mBufferA = new Dibit[144];
private final Dibit[] mBufferB = new Dibit[144];
//Soft symbols (radians) parallel to the dibit buffers, or NaN when the soft symbol wasn't provided
private final float[] mSoftBufferA = new float[144];
private final float[] mSoftBufferB = new float[144];
private int mBufferAPointer = 0;
private int mBufferBPointer = 0;
private DMRSyncPattern mBufferAPattern = DMRSyncPattern.UNKNOWN;
@@ -67,15 +89,80 @@ public class DMRMessageFramer implements Listener<Dibit>
{
Arrays.fill(mBufferA, Dibit.D00_PLUS_1);
Arrays.fill(mBufferB, Dibit.D00_PLUS_1);
Arrays.fill(mSoftSymbolDelayLine, Float.NaN);
Arrays.fill(mSoftBufferA, Float.NaN);
Arrays.fill(mSoftBufferB, Float.NaN);
mMessageFactory = new DMRMessageFactory(crcMaskManager);
}
/**
* Sets the sync pattern for the next C4FM sync detection notification via syncDetected().
* @param pattern that was detected.
*/
public void setSyncPattern(DMRSyncPattern pattern)
{
mSyncPattern = pattern;
}
/**
* C4FM symbol stream input method. Dibits are delayed via the burst delay line before framing.
* @param symbol to process
* @return true if a valid message was dispatched.
*/
@Override
public boolean process(Dibit symbol)
{
return process(symbol, Float.NaN);
}
/**
* C4FM symbol stream input method. Dibits and soft symbols are delayed via the burst delay lines before framing.
* @param symbol decision
* @param softSymbol equalized soft symbol in radians
* @return true if a valid message was dispatched.
*/
@Override
public boolean process(Dibit symbol, float softSymbol)
{
float ejectedSoftSymbol = mSoftSymbolDelayLine[mSoftSymbolDelayLinePointer];
mSoftSymbolDelayLine[mSoftSymbolDelayLinePointer++] = softSymbol;
mSoftSymbolDelayLinePointer %= DELAY_LINE_LENGTH;
return processDibit(mDibitDelayLine.insert(symbol), ejectedSoftSymbol);
}
/**
* C4FM sync detection notification where the most recently processed dibit is the final dibit of the sync pattern
* that was set via setSyncPattern(). Overwrites the sync dibits in the delay line with the detected sync pattern
* to eliminate any sync bit errors and signals that the next dibit ejected from the delay line is the first dibit
* of the burst.
*/
@Override
public void syncDetected()
{
if(DMRSyncPattern.SYNC_PATTERNS.contains(mSyncPattern))
{
mDibitDelayLine.update(mSyncPattern.toDibits());
syncDetected(mSyncPattern);
}
}
/**
* Primary input method for receiving the stream of demodulated dibit symbols.
* @param dibit to process.
*/
@Override
public void receive(Dibit dibit)
{
processDibit(dibit, Float.NaN);
}
/**
* Processes the dibit
* @param dibit to process
* @param softSymbol for the dibit in radians, or NaN.
* @return true if a valid message was dispatched.
*/
private boolean processDibit(Dibit dibit, float softSymbol)
{
mDibitCounter++;
mDibitSinceTimestampCounter++;
@@ -93,23 +180,27 @@ public class DMRMessageFramer implements Listener<Dibit>
{
if(mBufferAActive)
{
mSoftBufferA[mBufferAPointer] = softSymbol;
mBufferA[mBufferAPointer++] = dibit;
if(mBufferAPointer >= mBufferA.length)
{
dispatchBufferA();
return dispatchBufferA();
}
}
else
{
mSoftBufferB[mBufferBPointer] = softSymbol;
mBufferB[mBufferBPointer++] = dibit;
if(mBufferBPointer >= mBufferB.length)
{
dispatchBufferB();
return dispatchBufferB();
}
}
}
return false;
}
/**
@@ -121,7 +212,11 @@ public class DMRMessageFramer implements Listener<Dibit>
(!mBufferAActive && mBufferBPattern.isVoicePattern()));
}
private void dispatchBufferA()
/**
* Dispatches the burst assembled in buffer A.
* @return true if the dispatched burst message is valid.
*/
private boolean dispatchBufferA()
{
mDibitCounter -= 144;
@@ -150,7 +245,8 @@ public class DMRMessageFramer implements Listener<Dibit>
mDibitCounter = 0;
CorrectedBinaryMessage message = getMessage(mBufferA);
CACH cach = CACH.getCACH(message);
float[] softBits = getSoftBits(mSoftBufferA);
CACH cach = CACH.getCACH(message, softBits);
if(mBufferAPattern.hasCACH() && cach.isValid() && mBufferATimeslot != cach.getTimeslot())
{
@@ -167,7 +263,9 @@ public class DMRMessageFramer implements Listener<Dibit>
}
}
dispatch(mMessageFactory.create(mBufferAPattern, message, cach, getTimestamp(), mBufferATimeslot));
IMessage burstMessage = mMessageFactory.create(mBufferAPattern, message, softBits, cach,
getTimestamp(), mBufferATimeslot);
dispatch(burstMessage);
//Since voice frames only have sync on the first burst, we use pseudo-patterns to track the rest of the bursts
//across the voice super-frame. If the current pattern is a voice frame, set it to the next pseudo voice sync.
@@ -187,9 +285,15 @@ public class DMRMessageFramer implements Listener<Dibit>
{
mAssemblingBurst = false;
}
return burstMessage != null && burstMessage.isValid();
}
private void dispatchBufferB()
/**
* Dispatches the burst assembled in buffer B.
* @return true if the dispatched burst message is valid.
*/
private boolean dispatchBufferB()
{
mDibitCounter -= 144;
@@ -218,7 +322,8 @@ public class DMRMessageFramer implements Listener<Dibit>
mDibitCounter = 0;
CorrectedBinaryMessage burst = getMessage(mBufferB);
CACH cach = CACH.getCACH(burst);
float[] softBits = getSoftBits(mSoftBufferB);
CACH cach = CACH.getCACH(burst, softBits);
if(mBufferBPattern.hasCACH() && cach.isValid() && mBufferBTimeslot != cach.getTimeslot())
{
@@ -236,7 +341,9 @@ public class DMRMessageFramer implements Listener<Dibit>
}
}
dispatch(mMessageFactory.create(mBufferBPattern, burst, cach, getTimestamp(), mBufferBTimeslot));
IMessage burstMessage = mMessageFactory.create(mBufferBPattern, burst, softBits, cach,
getTimestamp(), mBufferBTimeslot);
dispatch(burstMessage);
//Since voice frames only have sync on the first burst, we use pseudo-patterns to track the rest of the bursts
//across the voice super-frame. If the current pattern is a voice frame, set it to the next pseudo voice sync.
@@ -256,6 +363,8 @@ public class DMRMessageFramer implements Listener<Dibit>
{
mAssemblingBurst = false;
}
return burstMessage != null && burstMessage.isValid();
}
/**
@@ -295,6 +404,28 @@ public class DMRMessageFramer implements Listener<Dibit>
mMessageListener = listener;
}
/**
* Converts the soft symbol buffer into soft bits (one per message bit) where a positive value favors a 1 bit.
* @param softSymbols buffer
* @return soft bits or null if any of the soft symbols were not provided.
*/
private static float[] getSoftBits(float[] softSymbols)
{
float[] softBits = new float[softSymbols.length * 2];
for(int x = 0; x < softSymbols.length; x++)
{
if(Float.isNaN(softSymbols[x]))
{
return null;
}
setSoftBits(softSymbols[x], softBits, x * 2);
}
return softBits;
}
/**
* Creates a binary message from the dibit buffer.
*

View File

@@ -187,20 +187,21 @@ public class DMRMessageProcessor implements Listener<IMessage>
if(message.getTimeslot() == 1)
{
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
voice.getFLCFragment(), message.getTimestamp());
voice.getFLCFragment(), voice.getFLCFragmentSoftBits(), message.getTimestamp());
receive(flco);
}
else
{
FullLCMessage flco = mFLCAssemblerTimeslot2.process(voice.getEMB().getLCSS(),
voice.getFLCFragment(), message.getTimestamp());
voice.getFLCFragment(), voice.getFLCFragmentSoftBits(), message.getTimestamp());
receive(flco);
}
if(voice.hasCACH())
{
CACH cach = voice.getCACH();
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(),
cach.getPayloadSoftBits(), message.getTimestamp());
receive(slco);
}
}
@@ -210,7 +211,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
if(dmrBurst.hasCACH())
{
CACH cach = dmrBurst.getCACH();
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(),
cach.getPayloadSoftBits(), message.getTimestamp());
receive(slco);
}

View File

@@ -1,766 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitDelayLine;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.gui.viewer.sync.SyncResultsViewer;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetector;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetectorFactory;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncDetectMode;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncModeMonitor;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
/**
* DMR soft symbol processor. Processes a stream of differentially decoded samples to align to the symbol timing and
* symbol period and extract symbols from the sample stream.
*
* Uses two soft sync correlation detectors to achieve initial fine sync with a primary sync detector and a lagging
* sync detector sampling at 1/2 the symbol period to ensure the sync is detected while only calculating at each symbol
* period interval. Once a sync is detected, fine sync is declared and the lagging sync detector is disabled. Fine sync
* disables whenever the quantity of processed symbols exceeds one DMR burst length and the lagging detector
* re-enables.
*
* The sync mode monitor processes the sync detections to identify the dominant sync mode for the channel and then
* disables processing of the two unused sync modes (BASE STATION, MOBILE STATION, or DIRECT MODE) for the remainder
* of the channel.
*
* This processor uses a symbol delay buffer (dibit delay line) to retain a partial DMR burst from CACH up through the
* sync field of the burst. This allows the optimize method to resample the burst symbols once the sync is detected and
* optimal symbol timing and symbol period are calculated. This ensures the best symbol decisions are then passed to
* the message framer along with the sync detection.
*
* Buffer Structure & Size = 129 Dibits: [2 pad][12 CACH][54 Message Prefix][24 Sync][24 workspace][1 pad]
*
* The equalizer calculates balance and gain values to apply to the differentially decoded sample stream to optimize
* the constellation skew and openness. The equalizer is updated at each quality sync detection and then the equalizer
* values are applied across the entire burst through to the next sync detection. DMR sync patterns are made up of
* equal quantities of +3 and -3 symbols which support the equalizer update approach.
*/
public class DMRSoftSymbolProcessor
{
private static final int BUFFER_PROTECTED_REGION_DIBITS = 92;
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS;
private static final float EQUALIZER_LOOP_GAIN = 0.15f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
private static final float MAXIMUM_POSITIVE_SAMPLE_PHASE = 3.5f;
private static final float MAXIMUM_NEGATIVE_SAMPLE_PHASE = -3.5f;
private static final float SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION = 0.005f; //.5%
private static final float SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
private static final float SYNC_DETECTION_THRESHOLD = 60;
private static final float SYNC_OPTIMIZED_THRESHOLD = 80;
private static final float SYNC_EQUALIZED_THRESHOLD = 100;
private static final float TWO_PI = (float)(Math.PI * 2.0);
private DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
private DMRSoftSyncDetector mSyncDetector = DMRSoftSyncDetectorFactory.getDetector();
private DMRSoftSyncDetector mSyncDetectorSecondary = DMRSoftSyncDetectorFactory.getDetector();
private DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300);
//Dibit delay line sizing: CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
private DibitDelayLine mDibitDelayLine = new DibitDelayLine(90);
private DMRMessageFramer mMessageFramer;
private boolean mFineSync = false;
private boolean mEqualizerInitialized = false;
private double mNoiseStandardDeviationThreshold;
private double mSecondarySyncOffset;
private double mSamplesPerSymbol;
private double mObservedSamplesPerSymbol;
private double mSamplePoint;
private float mEqualizerBalance = 0.0f;
private float mEqualizerGain = 1.0f;
private float mOptimizeFineIncrement;
private float[] mBuffer;
private int mBufferInterpolatorReservedRegion;
private int mBufferLoadPointer;
private int mBufferPointer;
private int mBufferWorkspaceLength;
private int mSymbolsSinceLastSync = 0;
private SyncResultsViewer mSyncResultsViewer;
private FeedbackDecoder mFeedbackDecoder;
/**
* Constructs an instance
* @param messageFramer to receive demodulated symbols/dibits and sync detect notifications
*/
public DMRSoftSymbolProcessor(DMRMessageFramer messageFramer, FeedbackDecoder feedbackDecoder)
{
if(messageFramer == null)
{
throw new IllegalArgumentException("Message framer cannot be null");
}
mMessageFramer = messageFramer;
mFeedbackDecoder = feedbackDecoder;
mSyncModeMonitor.add(mSyncDetector);
mSyncModeMonitor.add(mSyncDetectorSecondary);
}
/**
* Sets base station sync detection mode. This disables detection for MOBILE and DIRECT sync patterns. This can
* be used for traffic channels that are known to be part of a base station.
* @param enabled true for base mode only, false otherwise.
*/
public void setBaseStationMode(boolean enabled)
{
mSyncModeMonitor.setMode(enabled ? DMRSyncDetectMode.BASE_ONLY : DMRSyncDetectMode.AUTOMATIC);
}
/**
* Registers the listener to receive demodulated bit stream buffers.
* @param listener to register
*/
public void setBufferListener(Listener<ByteBuffer> listener)
{
mDibitAssembler.setBufferListener(listener);
}
/**
* Indicates if there is a registered buffer listener
*/
public boolean hasBufferListener()
{
return mDibitAssembler.hasBufferListeners();
}
/**
* Sets the reference timestamp from the incoming sample stream to use for assigning a timestamp to framed messages.
* @param timestamp to set for reference
*/
public void setTimestamp(long timestamp)
{
mMessageFramer.setTimestamp(timestamp);
}
/**
* Primary input method for receiving a stream of demodulated samples to process into symbols.
* @param samples to process
*/
public void receive(float[] samples)
{
int samplesPointer = 0;
while(samplesPointer < samples.length)
{
if(mBufferLoadPointer == mBuffer.length)
{
System.arraycopy(mBuffer, mBufferWorkspaceLength, mBuffer, 0, mBuffer.length - mBufferWorkspaceLength);
mBufferLoadPointer -= mBufferWorkspaceLength;
mBufferPointer -= mBufferWorkspaceLength;
}
int copyLength = Math.min(mBuffer.length - mBufferLoadPointer, samples.length - samplesPointer);
System.arraycopy(samples, samplesPointer, mBuffer, mBufferLoadPointer, copyLength);
samplesPointer += copyLength;
mBufferLoadPointer += copyLength;
for(int x = mBufferPointer; x < mBufferLoadPointer; x++)
{
//Unwrap phases
if(mBuffer[x - 1] > 1.5f && mBuffer[x] < -1.5f)
{
mBuffer[x] += TWO_PI;
}
else if(mBuffer[x - 1] < -1.5f && mBuffer[x] > 1.5f)
{
mBuffer[x] -= TWO_PI;
}
//Apply equalizer adjustments
mBuffer[x] += mEqualizerBalance;
mBuffer[x] *= mEqualizerGain;
//Allow the equalized buffer samples to exceed PI (3.14) by a small factor (3.5) to ensure the optimize
// and equalizer update functions work correctly. The toSymbol() method will correctly map any symbols
// that exceed +/- PI into the correct quadrant.
mBuffer[x] = Math.min(mBuffer[x], MAXIMUM_POSITIVE_SAMPLE_PHASE);
mBuffer[x] = Math.max(mBuffer[x], MAXIMUM_NEGATIVE_SAMPLE_PHASE);
}
float softSymbol, primaryScore, secondaryScore;
double secondary;
int secondaryIntegral;
while(mBufferPointer < (mBufferLoadPointer - mBufferInterpolatorReservedRegion))
{
mBufferPointer++;
mSamplePoint--;
if(mSamplePoint < 1)
{
if(mSymbolsSinceLastSync > 144)
{
mFineSync = false;
}
softSymbol = LinearInterpolator.calculate(mBuffer[mBufferPointer], mBuffer[mBufferPointer + 1], mSamplePoint);
mFeedbackDecoder.broadcast(softSymbol);
mSyncDetector.process(softSymbol);
Dibit symbol = toSymbol(softSymbol);
//Store the symbol in the delay line and broadcast the delayed ejected symbol to the message framer
//and the bitstream assembler.
Dibit ejected = mDibitDelayLine.insert(symbol);
mMessageFramer.receive(ejected);
mDibitAssembler.receive(ejected);
mSymbolsSinceLastSync++;
if(mFineSync)
{
if(mSymbolsSinceLastSync >= 144)
{
if(mMessageFramer.isVoiceSuperFrame())
{
mSymbolsSinceLastSync -= 144;
}
else
{
primaryScore = mSyncDetector.calculate();
//Debug - visualize the sync to show samples and symbol timing
// if(primaryScore > SYNC_DETECTION_THRESHOLD)
// {
// visualizeSyncDetect(mSyncDetector.getDetectedPattern(), primaryScore, true);
// }
if(primaryScore > SYNC_DETECTION_THRESHOLD && optimizeFine(mSyncDetector.getDetectedPattern()))
{
//Update the sync mode monitor and message framer
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
mSymbolsSinceLastSync = 0;
}
else
{
mFineSync = false;
mSyncDetectorSecondary.reset();
secondary = mBufferPointer + mSamplePoint - mSecondarySyncOffset;
secondaryIntegral = (int)Math.floor(secondary);
softSymbol = LinearInterpolator.calculate(mBuffer[secondaryIntegral], mBuffer[secondaryIntegral + 1], secondary - secondaryIntegral);
mSyncDetectorSecondary.process(softSymbol);
}
}
}
}
else
{
primaryScore = mSyncDetector.calculate();
secondary = mBufferPointer + mSamplePoint - mSecondarySyncOffset;
secondaryIntegral = (int)Math.floor(secondary);
softSymbol = LinearInterpolator.calculate(mBuffer[secondaryIntegral], mBuffer[secondaryIntegral + 1], secondary - secondaryIntegral);
secondaryScore = mSyncDetectorSecondary.processAndCalculate(softSymbol);
if(primaryScore > SYNC_DETECTION_THRESHOLD && optimizeCoarse(mSyncDetector.getDetectedPattern(), 0))
{
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
mFineSync = true;
mSymbolsSinceLastSync = 0;
}
else if(secondaryScore > SYNC_DETECTION_THRESHOLD && optimizeCoarse(mSyncDetectorSecondary.getDetectedPattern(), -mSecondarySyncOffset))
{
mMessageFramer.syncDetected(mSyncDetectorSecondary.getDetectedPattern());
mFineSync = true;
mSymbolsSinceLastSync = 0;
}
}
//Add another symbol's worth of samples to the counter
mSamplePoint += mObservedSamplesPerSymbol;
}
}
}
}
/**
* Indicates if the samples in the sample buffer that contain a detected sync pattern have a standard deviation
* that is lower than the expected sample to sample deviation for a modulated signal. False detects against
* noise tend to have a standard deviation that is 2x the expected value.
* @param offset to the sample representing the final symbol in the detected sync pattern.
* @return true if the standard deviation is more than expected.
*/
private boolean isNoisy(double offset)
{
StandardDeviation standardDeviation = new StandardDeviation();
int start = (int)Math.floor(offset - (23 * mObservedSamplesPerSymbol));
int end = (int)Math.ceil(offset);
end = Math.min(end, mBuffer.length - 1);
for(int i = start; i < end; i++)
{
standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]);
}
return standardDeviation.getResult() > mNoiseStandardDeviationThreshold;
}
/**
* Adjusts the symbol timing and symbol spacing to identify the best achievable sync correlation score and apply
* those adjustments when the correlation score exceeds a positive sync detection threshold.
* @param pattern that was detected
* @param additionalOffset from current mBufferPointer and mSamplePoint. This can be zero offset for the primary
* sync detector or an offset for the lagging sync detectors.
* @return true if there is a positive sync detection.
*/
private boolean optimizeCoarse(DMRSyncPattern pattern, double additionalOffset)
{
//Offset is the start of the first sample of the first symbol of the sync pattern calculated from the current
//buffer pointer and sample point which should be the final sample of the final symbol of the detected sync.
double offset = mBufferPointer + mSamplePoint + additionalOffset;
//Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold.
if(isNoisy(offset))
{
return false;
}
//Find the optimal symbol timing
double stepSize = mSamplesPerSymbol / 8.0; //Start at 1/8th of the samples per symbol
double stepSizeMin = mOptimizeFineIncrement;
double adjustment = 0.0;
double adjustmentMax = mSamplesPerSymbol / 4.0;
double candidate = offset;
float scoreCenter = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset - stepSize;
float scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + stepSize;
float scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax)
{
if(scoreLeft > scoreRight && scoreLeft > scoreCenter)
{
adjustment -= stepSize;
scoreRight = scoreCenter;
scoreCenter = scoreLeft;
candidate = offset + adjustment - stepSize;
scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
}
else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
{
adjustment += stepSize;
scoreLeft = scoreCenter;
scoreCenter = scoreRight;
candidate = offset + adjustment + stepSize;
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
else
{
stepSize *= 0.5f;
if(stepSize > stepSizeMin)
{
candidate = offset + adjustment - stepSize;
scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + adjustment + stepSize;
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
}
}
//If we didn't find an optimal correlation score above the 95 threshold, return a false sync.
if(scoreCenter < 95)
{
return false;
}
adjustment += additionalOffset;
mSamplePoint += adjustment;
while(mSamplePoint < 0)
{
mSamplePoint++;
mBufferPointer--;
}
while(mSamplePoint > 1)
{
mSamplePoint--;
mBufferPointer++;
}
boolean resample = !mEqualizerInitialized || (Math.abs(adjustment) > 0.25);
updateEqualizer(pattern);
// visualizeSyncDetect(pattern, scoreCenter, true);
//If the equalizer was just initialized or the timing error adjustment is high enough, resample the symbols.
// Otherwise, overwrite the captured sync pattern in the delay buffer with the detected sync pattern to
// eliminate any sync bit errors.
if(resample)
{
double resamplePointer = mBufferPointer + mSamplePoint;
resamplePointer -= (89 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
int integral;
for(int x = 0; x < 66; x++)
{
integral = (int)Math.floor(resamplePointer);
if(integral >= 0)
{
float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[integral], mBuffer[integral + 1], resamplePointer - integral);
mDibitDelayLine.insert(toSymbol(resampledSoftSymbol));
}
else
{
//This shouldn't happen since there's 2x dibits of padding on the front side, but just in case.
mDibitDelayLine.insert(Dibit.D01_PLUS_3);
}
resamplePointer += mObservedSamplesPerSymbol;
}
//We don't need to resample the sync region ... just use the actual sync dibit values.
for(Dibit dibit: pattern.toDibits())
{
mDibitDelayLine.insert(dibit);
}
}
else
{
//Overwrite the most recent 24 dibits with the detected sync so there's no sync bit errors
mDibitDelayLine.update(pattern.toDibits());
}
return true;
}
/**
* Performs fine-grained symbol timing optimization of +/- .4% of the samples per symbol as needed.
* @param pattern that was detected
* @return true if detection score exceeds the optimized and equalized threshold for a quality sync detect.
*/
private boolean optimizeFine(DMRSyncPattern pattern)
{
//Offset is the sample for the last symbol of the detected sync pattern.
double offset = mBufferPointer + mSamplePoint;
float currentScore = score(offset, mObservedSamplesPerSymbol, pattern);
double candidate = offset - mOptimizeFineIncrement;
float candidateScore = score(candidate, mObservedSamplesPerSymbol, pattern);
boolean adjusted = false;
if(candidateScore > currentScore)
{
mSamplePoint -= mOptimizeFineIncrement;
adjusted = true;
}
else
{
candidate = offset + mOptimizeFineIncrement;
candidateScore = score(candidate, mObservedSamplesPerSymbol, pattern);
if(candidateScore > currentScore)
{
mSamplePoint += mOptimizeFineIncrement;
adjusted = true;
}
}
if(adjusted)
{
while(mSamplePoint < 0)
{
mSamplePoint++;
mBufferPointer--;
}
while(mSamplePoint > 1)
{
mSamplePoint--;
mBufferPointer++;
}
}
if(candidateScore > SYNC_OPTIMIZED_THRESHOLD)
{
updateEqualizer(pattern);
}
return candidateScore > SYNC_EQUALIZED_THRESHOLD;
}
/**
* Update the equalizer balance and gain when the sync pattern is detected in the sample buffer. This method
* resamples the symbols and compares each soft symbol to the ideal symbol phase to develop average error
* measurements for balance and gain. On initial sync detection, the equalizer settings are applied to the
* samples in the buffer allowing the symbols to be resampled during coarse sync acquisition.
* @param pattern that was detected.
*/
private void updateEqualizer(DMRSyncPattern pattern)
{
float[] symbols = pattern.toSymbols();
double resampleStart = mBufferPointer + mSamplePoint;
int resampleStartIntegral = (int)Math.floor(resampleStart);
float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
float balanceAccumulator = resampledSoftSymbol - symbols[23];
float gainAccumulator = Math.abs(symbols[23]) - Math.abs(resampledSoftSymbol);
resampleStart -= (23 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
resampleStartIntegral = (int)Math.floor(resampleStart);
for(int x = 0; x < 23; x++)
{
if(resampleStartIntegral >= 0)
{
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
balanceAccumulator += resampledSoftSymbol - symbols[x];
gainAccumulator += Math.abs(symbols[x]) - Math.abs(resampledSoftSymbol);
}
resampleStart += mObservedSamplesPerSymbol;
resampleStartIntegral = (int)Math.floor(resampleStart);
}
//Average the accumulated error over 24 sync symbols
balanceAccumulator /= -24.0f;
gainAccumulator /= (24.0f * Dibit.D01_PLUS_3.getIdealPhase());
if(mEqualizerInitialized)
{
//Limit equalizer adjustments at each sync after the initial equalizer setup.
mEqualizerBalance += (balanceAccumulator * EQUALIZER_LOOP_GAIN);
mEqualizerGain += (gainAccumulator * EQUALIZER_LOOP_GAIN);
}
else
{
mEqualizerBalance += balanceAccumulator;
mEqualizerGain += gainAccumulator;
}
//Constrain balance to +/- PI/4
mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE);
mFeedbackDecoder.processPLLError(mEqualizerBalance);
//Constrain gain between 1.0f and 1.35f
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);
mEqualizerGain = Math.max(mEqualizerGain, 1.0f);
if(!mEqualizerInitialized)
{
//Apply the initial gain settings to the samples in the buffer so that the symbols can be resampled.
for(int x = 0; x < mBufferPointer; x++)
{
mBuffer[x] = (mBuffer[x] + mEqualizerBalance) * mEqualizerGain;
}
mEqualizerInitialized = true;
}
}
/**
* Debug method to visualize the contents of the sample buffer.
*/
public void visualizeBufferContents()
{
//This will block until the viewer is constructed and showing.
if(mSyncResultsViewer == null)
{
mSyncResultsViewer = new SyncResultsViewer();
}
int offset = 3;
int length = (int)Math.ceil(mObservedSamplesPerSymbol * 90) + (2 * offset);
int end = mBufferPointer + offset;
int start = end - length;
float[] symbols = new float[90];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
float[] intervals = new float[90];
int adjust = mBufferPointer - length + offset;
double pointer = mBufferPointer + mSamplePoint - adjust;
for(int x = 89; x >= 0; x--)
{
intervals[x] = (float)pointer;
pointer -= mObservedSamplesPerSymbol;
}
float[] sync = new float[90];
Arrays.fill(sync, -1.57f); //Cutoff between -1 & -1 to show symbol timings
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, sync, samples, intervals, mEqualizerBalance, mEqualizerGain,
"BUFFER CONTENTS", countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
* @param pattern that was detected.
* @param score from the sync detector
* @param primary sync detector (true) or secondary (false)
*/
public void visualizeSyncDetect(DMRSyncPattern pattern, float score, boolean primary)
{
//This will block until the viewer is constructed and showing.
if(mSyncResultsViewer == null)
{
mSyncResultsViewer = new SyncResultsViewer();
}
int offset = 3;
int length = (int)Math.ceil(mObservedSamplesPerSymbol * 23) + (2 * offset);
int end = mBufferPointer + offset;
int start = end - length;
float[] symbols = new float[24];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
float[] intervals = new float[24];
int adjust = mBufferPointer - length + offset;
double pointer = mBufferPointer + mSamplePoint - adjust;
double symbolPointer = mBufferPointer + mSamplePoint - (mObservedSamplesPerSymbol * 23);
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < 24; x++)
{
symbols[x] = LinearInterpolator.calculate(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
symbolPointer += mObservedSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
for(int x = 23; x >= 0; x--)
{
intervals[x] = (float)pointer;
pointer -= mObservedSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, pattern.toSymbols(), samples, intervals, mEqualizerBalance, mEqualizerGain,
pattern + " Score: " + score + (primary ? " PRIMARY" : " SECONDARY"), countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Calculates the sync correlation score for sync pattern ending at the specified offset and samples per symbol interval.
* @param offset to the final symbol in the soft symbol buffer
* @param samplesPerSymbol spacing to test for.
* @param pattern to correlate against.
* @return correlation score.
*/
public float score(double offset, double samplesPerSymbol, DMRSyncPattern pattern)
{
float[] symbols = pattern.toSymbols();
int maxPointer = mBuffer.length - 1;
float softSymbol;
double pointer = offset - (samplesPerSymbol * 23.0);
int bufferPointer = (int)Math.floor(pointer);
double fractional = pointer - bufferPointer;
float score = 0;
for(int x = 0; x < 24; x++)
{
if(bufferPointer < maxPointer)
{
softSymbol = LinearInterpolator.calculate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional);
}
else
{
softSymbol = 0.0f;
}
score += softSymbol * symbols[x];
pointer += samplesPerSymbol;
bufferPointer = (int)Math.floor(pointer);
fractional = pointer - bufferPointer;
}
return score;
}
/**
* Sets or updates the samples per symbol
* @param samplesPerSymbol to apply.
*/
public void setSamplesPerSymbol(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
float maxSamplesPerSymbol = samplesPerSymbol * (1.0f + SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION);
mOptimizeFineIncrement = samplesPerSymbol * .004f; //Adjust at .4%
//Protected region for score() method to avoid IndexOutOfBounds = Max Adjustment + Max Step Size for Max SPS
mBufferInterpolatorReservedRegion = (int)Math.ceil((maxSamplesPerSymbol / 2.0f) + (maxSamplesPerSymbol / 10.0f));
mObservedSamplesPerSymbol = mSamplesPerSymbol;
mNoiseStandardDeviationThreshold = Dibit.D01_PLUS_3.getIdealPhase() * 2 / mSamplesPerSymbol * 1.2; //120% of optimal
mSamplePoint = mSamplesPerSymbol;
mSecondarySyncOffset = mSamplesPerSymbol / 2.0;
mBufferWorkspaceLength = (int)Math.ceil(BUFFER_WORKSPACE_LENGTH_DIBITS * mSamplesPerSymbol);
int bufferLength = (int)(Math.ceil(BUFFER_LENGTH_DIBITS * mSamplesPerSymbol));
mBuffer = new float[bufferLength];
mBufferLoadPointer = (int)Math.ceil(BUFFER_PROTECTED_REGION_DIBITS * mSamplesPerSymbol);
mBufferPointer = mBufferLoadPointer;
}
/**
* Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
* @param sample in radians.
* @return symbol decision.
*/
private static Dibit toSymbol(float sample)
{
if(sample > 0)
{
return sample > SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
}
else
{
return sample < -SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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;
import io.github.dsheirer.dsp.c4fm.C4FMSymbolProcessor;
import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetector;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetectorFactory;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncDetectMode;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncModeMonitor;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
/**
* DMR implementation of the C4FM symbol processor.
*
* DMR employs multiple sync patterns (BASE STATION, MOBILE STATION and DIRECT MODE, each with DATA and VOICE variants)
* and the sync detectors correlate against all active sync patterns, reporting the best scoring pattern. The sync mode
* monitor processes the sync detections to identify the dominant sync mode for the channel and then disables
* processing of the two unused sync modes for the remainder of the channel.
*
* On each sync detection, the detected sync pattern is passed to the message framer which maintains a delay line to
* retain the burst dibits from the CACH through the sync pattern so that the full burst can be framed.
*
* DMR sync patterns are made up of equal quantities of +3 and -3 symbols which supports the default (mean) balance
* measurement approach of the parent class.
*/
public class DMRSymbolProcessor extends C4FMSymbolProcessor
{
private static final int BURST_DIBIT_LENGTH = 144;
private static final int SYNC_DIBIT_LENGTH = 24;
//Maximum sync interval for data bursts is two bursts for single timeslot (ie mobile/direct mode) data.
private static final int MAX_DATA_SYNC_INTERVAL = BURST_DIBIT_LENGTH * 2 + SYNC_DIBIT_LENGTH;
//Maximum sync interval for a voice super frame is 6x bursts per timeslot for single timeslot (ie direct mode) voice.
private static final int MAX_VOICE_SYNC_INTERVAL = BURST_DIBIT_LENGTH * 12 + SYNC_DIBIT_LENGTH;
//Training period is limited to the remainder of the burst that follows the sync since the next burst can be
//empty (ie noise) for mobile station and direct mode channels. Testing: longer periods (120-144) gained less
//than 0.2% on base station recordings, which doesn't justify the risk of training on empty timeslots.
private static final int EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT = 54;
private static final int EQUALIZER_SKIP_TRAINING_FRAME_COUNT = 10;
private static final int EQUALIZER_SYMBOL_LENGTH = 7;
private static final int SYMBOL_RATE = 4800;
private static final double OUTER_SYMBOL_DEVIATION = 1944.0; //Hertz
//Sync thresholds. Testing against DMR recordings and injected noise: lowering the optimized and equalized
//thresholds from 100 to 70 (60 - 70 perform the same) increased valid bursts 5% on multipath recordings and
//7 - 27% with injected noise. Lowering the detection threshold below 80 floods the optimizer with false syncs.
private static final float SYNC_THRESHOLD_DETECTED = 80;
private static final float SYNC_THRESHOLD_OPTIMIZED = 70;
private static final float SYNC_THRESHOLD_EQUALIZED = 70;
//Once synchronized, sync detections are only allowed within this many symbols of a burst boundary.
private static final int SYNC_WINDOW_SYMBOLS = 2;
private final DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
private final DMRMessageFramer mMessageFramer;
/**
* Constructs an instance
*
* @param feedbackDecoder parent decoder
* @param messageFramer to receive symbol decisions (dibits) and sync notifications.
*/
public DMRSymbolProcessor(FeedbackDecoder feedbackDecoder, DMRMessageFramer messageFramer)
{
this(feedbackDecoder, messageFramer, DMRSoftSyncDetectorFactory.getDetector(),
DMRSoftSyncDetectorFactory.getDetector());
}
/**
* Constructs an instance
*
* @param feedbackDecoder parent decoder
* @param messageFramer to receive symbol decisions (dibits) and sync notifications.
* @param primary sync detector
* @param lagging sync detector
*/
private DMRSymbolProcessor(FeedbackDecoder feedbackDecoder, DMRMessageFramer messageFramer,
DMRSoftSyncDetector primary, DMRSoftSyncDetector lagging)
{
super(feedbackDecoder, messageFramer, primary, lagging);
mMessageFramer = messageFramer;
mSyncModeMonitor.add(primary);
mSyncModeMonitor.add(lagging);
}
/**
* Sets base station sync detection mode. This disables detection for MOBILE and DIRECT sync patterns. This can
* be used for traffic channels that are known to be part of a base station.
* @param enabled true for base mode only, false otherwise.
*/
public void setBaseStationMode(boolean enabled)
{
mSyncModeMonitor.setMode(enabled ? DMRSyncDetectMode.BASE_ONLY : DMRSyncDetectMode.AUTOMATIC);
}
/**
* Captures the detected sync pattern to update the sync mode monitor and the message framer.
* @param syncDetector that triggered the sync detection.
*/
@Override
protected void syncDetected(ISoftC4FMSyncDetector syncDetector)
{
if(syncDetector instanceof DMRSoftSyncDetector dmrSyncDetector)
{
DMRSyncPattern pattern = dmrSyncDetector.getDetectedPattern();
mSyncModeMonitor.detected(pattern);
mMessageFramer.setSyncPattern(pattern);
}
}
/**
* Once synchronized with at least two consecutive sync detections, only allows sync detections within a small
* window around each expected burst boundary (multiples of the burst length) to prevent false sync detections
* from burst payload (ie voice) symbols that would otherwise cause the framer to abandon the current burst.
* @param symbolsSinceLastSync count of symbols since the final symbol of the previous sync detection.
* @return true if a sync detection is allowed.
*/
@Override
protected boolean isSyncDetectionAllowed(int symbolsSinceLastSync)
{
if(!mSynchronized || getConsecutiveSyncPeriods() < 2)
{
return true;
}
int offset = symbolsSinceLastSync % BURST_DIBIT_LENGTH;
return Math.min(offset, BURST_DIBIT_LENGTH - offset) <= SYNC_WINDOW_SYMBOLS;
}
@Override
public boolean isDynamicBalanceTrackingRequired()
{
return true;
}
/**
* Maximum dibit interval between sync detections, which is longer while the framer is assembling a voice super
* frame where only the first burst of each super frame contains a sync pattern.
*/
@Override
public int getMaxMessageDibitLength()
{
return mMessageFramer.isVoiceSuperFrame() ? MAX_VOICE_SYNC_INTERVAL : MAX_DATA_SYNC_INTERVAL;
}
@Override
public int getEqualizerTrainingPeriodDurationSymbolCount()
{
return EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT;
}
@Override
public int getEqualizerSymbolLength()
{
return EQUALIZER_SYMBOL_LENGTH;
}
@Override
public int getEqualizerSkipTrainingFrameCount()
{
return EQUALIZER_SKIP_TRAINING_FRAME_COUNT;
}
@Override
public int getSymbolRate()
{
return SYMBOL_RATE;
}
@Override
public double getOuterSymbolDeviation()
{
return OUTER_SYMBOL_DEVIATION;
}
@Override
public float getSyncThresholdDetected()
{
return SYNC_THRESHOLD_DETECTED;
}
@Override
public float getSyncThresholdOptimized()
{
return SYNC_THRESHOLD_OPTIMIZED;
}
@Override
public float getSyncThresholdEqualized()
{
return SYNC_THRESHOLD_EQUALIZED;
}
}

View File

@@ -22,6 +22,8 @@ package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming16;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
/**
* Block product turbo code (BPTC) for decoding Full Link Control messages reassembled from the 32-bit payload fragments
@@ -29,6 +31,41 @@ import io.github.dsheirer.edac.Hamming16;
*/
public class BPTC_128_77 extends BPTCBase
{
private static final int LENGTH = 128;
private static final int ROW_COUNT = 8;
private static final int COLUMN_COUNT = 16;
//Maximum correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) for a soft
//decision result. The embedded link control is only protected by a 5-bit checksum, so soft decisions that
//require correcting high reliability bits are rejected to avoid false decodes. Noise injection testing: all
//soft decisions with a cost up to 3.0 were correct, whereas most of those with a higher cost were false decodes.
private static final float SOFT_MAX_CORRECTION_COST = 3.0f;
//Maximum corrected bit count for a hard decision result. Hard decisions that correct more bits are treated as
//uncorrectable to avoid false decodes, given the weak 5-bit checksum. Noise injection testing: all hard
//decisions with more than 6 corrected bits were false decodes.
private static final int HARD_MAX_CORRECTED_BITS = 6;
//Soft decision decoder for the Hamming(16,11,4) rows and even parity columns
private static final SoftProductCodeDecoder SOFT_DECODER = new SoftProductCodeDecoder(
SoftBlockCode.fromCodewordTest(COLUMN_COUNT, word -> Hamming16.getSyndrome(toMessage(word), 0) == 0),
ROW_COUNT, SOFT_MAX_CORRECTION_COST);
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word)
{
BinaryMessage message = new BinaryMessage(COLUMN_COUNT);
for(int i = 0; i < COLUMN_COUNT; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
public BPTC_128_77()
{
super(new Hamming16(), 16, 8);
@@ -49,10 +86,38 @@ public class BPTC_128_77 extends BPTCBase
* @return decoded message.
*/
public CorrectedBinaryMessage extract(BinaryMessage message)
{
return extract(message, null);
}
/**
* Performs error detection and correction and extracts the payload from the BPTC encoded message. Hard decision
* results that exceed the maximum corrected bit count are treated as uncorrectable. When the hard decision
* decoder can't correct the message and soft bits are provided, applies iterative soft decision decoding.
* @param message with BPTC encoding.
* @param softBits for the 128 (interleaved) bits of the message where a positive value favors a 1 bit, or null.
* @return decoded message.
*/
public CorrectedBinaryMessage extract(BinaryMessage message, float[] softBits)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
correct(deinterleaved);
if(deinterleaved.getCorrectedBitCount() > HARD_MAX_CORRECTED_BITS)
{
deinterleaved.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
}
if(softBits != null && softBits.length == LENGTH && deinterleaved.getCorrectedBitCount() < 0)
{
CorrectedBinaryMessage softDecoded = decodeSoft(deinterleave(softBits));
if(softDecoded != null)
{
deinterleaved = softDecoded;
}
}
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(77);
int pointer = 0;
@@ -80,6 +145,38 @@ public class BPTC_128_77 extends BPTCBase
return extractedMessage;
}
/**
* Deinterleaves the transmitted soft bits.
* @param interleaved soft bits
* @return deinterleaved soft bits.
*/
public static float[] deinterleave(float[] interleaved)
{
float[] deinterleaved = new float[LENGTH];
deinterleaved[127] = interleaved[127];
for(int i = 0; i < 127; i++)
{
deinterleaved[i] = interleaved[(i * 8) % 127];
}
return deinterleaved;
}
/**
* Iterative soft decision decoding of the de-interleaved BPTC(128,77) soft bits. Alternately decodes the
* Hamming(16,11,4) rows using Chase-Pyndiah decoding and the even parity columns using min-sum decoding,
* exchanging extrinsic soft information, until the decisions form a valid product codeword. Rejects decisions
* where the correction cost exceeds the maximum.
* @param channel de-interleaved soft bits where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge or the correction
* cost exceeds the maximum.
*/
public CorrectedBinaryMessage decodeSoft(float[] channel)
{
return SOFT_DECODER.decode(channel);
}
/**
* Deinterleaves the transmitted message.
* @param interleaved message

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.IHamming;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -42,6 +43,21 @@ public class BPTC_196_96
public static final int CHECKSUM_COLUMN_COUNT = 4;
public static final int MESSAGE_START_INDEX = 4;
public static final int MAXIMUM_RECURSION_TURBO_DEPTH = 20;
private static final int ROW_COUNT = 13;
private static final int ROW_LENGTH = 15;
private static final int COLUMN_LENGTH = 13;
//Soft decision (Chase-Pyndiah) iterative decoding settings. Each half iteration decodes either all rows or all
//columns, starting with the rows. Alpha scales the extrinsic information fed to the next half iteration and beta
//is the reliability assigned to bits without a competing candidate codeword.
private static final int CHASE_LEAST_RELIABLE_BITS = 4;
private static final float[] SOFT_ALPHA = new float[]{0.0f, 0.2f, 0.3f, 0.5f, 0.7f, 0.9f, 1.0f, 1.0f};
private static final float[] SOFT_BETA = new float[]{0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f};
private static final int[] COLUMN_WORD_INDEXES = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(ROW_LENGTH,
word -> Hamming15.getSyndrome(toMessage(word, ROW_LENGTH), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(COLUMN_LENGTH,
word -> Hamming13.getSyndrome(toMessage(word, COLUMN_LENGTH), COLUMN_WORD_INDEXES) == 0);
/**
* DMR De-interleaving indices
@@ -76,6 +92,40 @@ public class BPTC_196_96
{14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194},
{15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}};
/**
* DMR Row Indices
*/
public static final int[][] ROW_INDEXES = new int[ROW_COUNT][ROW_LENGTH];
static
{
for(int row = 0; row < ROW_COUNT; row++)
{
for(int x = 0; x < ROW_LENGTH; x++)
{
ROW_INDEXES[row][x] = getIndex(x, row);
}
}
}
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
* protected message
@@ -83,6 +133,19 @@ public class BPTC_196_96
* @return error corrected and extracted 96-bit message
*/
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
{
return extract(original, null);
}
/**
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
* protected message. When the hard decision decoder can't correct the message and soft bits are provided,
* applies iterative soft decision decoding.
* @param original uncorrected BPTC(196,96) message
* @param softBits for the 196 (interleaved) bits of the original message, or null.
* @return error corrected and extracted 96-bit message
*/
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original, float[] softBits)
{
//De-Interleave
CorrectedBinaryMessage message = deinterleave(original);
@@ -91,6 +154,43 @@ public class BPTC_196_96
boolean pursueShadows = true;
correct(message, pursueShadows);
if(softBits != null && softBits.length == BPTC_LENGTH && message.getCorrectedBitCount() < 0)
{
CorrectedBinaryMessage softDecoded = decodeSoft(deinterleave(softBits));
if(softDecoded != null)
{
message = softDecoded;
}
}
return extractPayload(message);
}
/**
* Applies soft decision decoding to the soft bits and extracts the 96-bit message. Can be used when a hard
* decision result fails a subsequent message integrity (ie CRC) check.
* @param softBits for the 196 (interleaved) bits of the message where a positive value favors a 1 bit.
* @return error corrected and extracted 96-bit message, or null if soft decision decoding doesn't converge.
*/
public static CorrectedBinaryMessage extractSoft(float[] softBits)
{
if(softBits == null || softBits.length != BPTC_LENGTH)
{
return null;
}
CorrectedBinaryMessage decoded = decodeSoft(deinterleave(softBits));
return decoded != null ? extractPayload(decoded) : null;
}
/**
* Extracts the 96-bit message and the 3 reserved bits from the corrected (de-interleaved) 196-bit message.
* @param message corrected and de-interleaved.
* @return extracted message with the corrected bit count transferred.
*/
private static CorrectedBinaryMessage extractPayload(CorrectedBinaryMessage message)
{
//Extract the 96-bit message from the corrected 196 bits
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
@@ -130,6 +230,174 @@ public class BPTC_196_96
return extracted;
}
/**
* De-interleaves the transmitted soft bits of a DMR BPTC(196,96) protected message
* @param softBits that contains interleaved (transmitted) soft bits
* @return de-interleaved soft bits
*/
private static float[] deinterleave(float[] softBits)
{
float[] deinterleaved = new float[BPTC_LENGTH];
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
{
deinterleaved[x] = softBits[BPTC_DEINTERLEAVE[x]];
}
return deinterleaved;
}
/**
* Iterative soft decision (Chase-Pyndiah turbo product) decoding of the de-interleaved BPTC(196,96) soft bits.
* Alternately decodes the Hamming(15,11) rows and Hamming(13,9) columns, exchanging extrinsic soft information,
* until the decisions form a valid product codeword.
* @param channel de-interleaved soft bits where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge.
*/
public static CorrectedBinaryMessage decodeSoft(float[] channel)
{
//Normalize the channel soft bits to a mean magnitude of 1 for the alpha and beta settings
float sum = 0.0f;
for(int i = 1; i < BPTC_LENGTH; i++)
{
sum += Math.abs(channel[i]);
}
if(sum <= 0.0f)
{
return null;
}
float scale = (BPTC_LENGTH - 1) / sum;
float[] received = new float[BPTC_LENGTH];
for(int i = 0; i < BPTC_LENGTH; i++)
{
received[i] = channel[i] * scale;
}
float[] extrinsic = new float[BPTC_LENGTH];
float[] input = new float[BPTC_LENGTH];
float[] output = new float[BPTC_LENGTH];
int[] rowWords = new int[ROW_COUNT];
int[] columnWords = new int[COLUMN_COUNT];
for(int halfIteration = 0; halfIteration < SOFT_ALPHA.length; halfIteration++)
{
float alpha = SOFT_ALPHA[halfIteration];
float beta = SOFT_BETA[halfIteration];
for(int i = 0; i < BPTC_LENGTH; i++)
{
input[i] = received[i] + alpha * extrinsic[i];
}
boolean decodeRows = halfIteration % 2 == 0;
boolean allDecoded = true;
if(decodeRows)
{
for(int row = 0; row < ROW_COUNT; row++)
{
rowWords[row] = ROW_CODE.decodeChase(input, ROW_INDEXES[row], output, CHASE_LEAST_RELIABLE_BITS, beta);
allDecoded &= rowWords[row] >= 0;
}
}
else
{
for(int column = 0; column < COLUMN_COUNT; column++)
{
columnWords[column] = COLUMN_CODE.decodeChase(input, COLUMN_INDEXES[column], output,
CHASE_LEAST_RELIABLE_BITS, beta);
allDecoded &= columnWords[column] >= 0;
}
}
//Check for a valid product codeword from the decisions of this half iteration
if(allDecoded)
{
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(BPTC_LENGTH);
if(received[0] > 0)
{
decoded.set(0);
}
int[][] decodedIndexes = decodeRows ? ROW_INDEXES : COLUMN_INDEXES;
int[] decodedWords = decodeRows ? rowWords : columnWords;
for(int w = 0; w < decodedWords.length; w++)
{
for(int i = 0; i < decodedIndexes[w].length; i++)
{
if((decodedWords[w] & (1 << i)) != 0)
{
decoded.set(decodedIndexes[w][i]);
}
}
}
if(isValidProductCodeword(decoded))
{
int corrected = 0;
for(int i = 0; i < BPTC_LENGTH; i++)
{
if(decoded.get(i) != (received[i] > 0))
{
corrected++;
}
}
decoded.setCorrectedBitCount(corrected);
return decoded;
}
}
//Normalize the extrinsic information to a mean magnitude of 1 for the next half iteration
float extrinsicSum = 0.0f;
for(int i = 1; i < BPTC_LENGTH; i++)
{
extrinsicSum += Math.abs(output[i]);
}
float extrinsicScale = extrinsicSum > 0.0f ? (BPTC_LENGTH - 1) / extrinsicSum : 0.0f;
for(int i = 0; i < BPTC_LENGTH; i++)
{
extrinsic[i] = output[i] * extrinsicScale;
}
}
return null;
}
/**
* Indicates if all rows and columns of the de-interleaved message pass the Hamming checks.
*/
private static boolean isValidProductCodeword(CorrectedBinaryMessage message)
{
for(int row = 0; row < ROW_COUNT; row++)
{
if(!isRowCorrect(row, message))
{
return false;
}
}
for(int column = 0; column < COLUMN_COUNT; column++)
{
if(!isColumnCorrect(column, message))
{
return false;
}
}
return true;
}
/**
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
* @param message that contains interleaved (transmitted) message
@@ -311,11 +579,36 @@ public class BPTC_196_96
* Performs turbo decode of the message by testing multiple paths across rows and columns to identify paths
* that resolve errors validated by the Hamming checks. Can also detect and often correct shadowing where the bit
* errors occur in a 2x2, 2x3, 3x2 or 3x3 column/row grid and can't normally be corrected by the Hamming code.
* When corrected, the message corrected bit count is set to the number of bits that differ from the received
* message.
*
* @param message to correct.
* @param pursueShadows to test intersection flipping for double/triple shadows.
* @return true if the message was corrected, false otherwise.
*/
public static boolean correct(CorrectedBinaryMessage message, boolean pursueShadows)
{
CorrectedBinaryMessage received = new CorrectedBinaryMessage(message);
boolean corrected = correctTurbo(message, pursueShadows);
if(corrected)
{
//The turbo decoder's incremental accounting can count a bit more than once, so measure the corrections
received.xor(message);
message.setCorrectedBitCount(received.cardinality());
}
return corrected;
}
/**
* Turbo decode implementation for the correct() method. Note: the corrected bit count set by this method is
* not accurate and is replaced by the correct() method.
* @param message to correct.
* @param pursueShadows to test intersection flipping for double/triple shadows.
* @return true if the message was corrected, false otherwise.
*/
private static boolean correctTurbo(CorrectedBinaryMessage message, boolean pursueShadows)
{
List<Integer> columnErrors = getColumnErrors(message);
List<Integer> rowErrors = getRowErrors(message);
@@ -413,7 +706,7 @@ public class BPTC_196_96
//Set pursue shadows to false so we don't recursively chase shadows
pursueShadows = false;
if(correct(message, pursueShadows))
if(correctTurbo(message, pursueShadows))
{
columns.clear();
rows.clear();

View File

@@ -22,12 +22,45 @@ package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming17;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
/**
* Block product turbo code (BPTC) for decoding Short Link Control reassembled from CACH fragments.
*/
public class BPTC_68_36 extends BPTCBase
{
private static final int LENGTH = 68;
private static final int ROW_COUNT = 4;
private static final int COLUMN_COUNT = 17;
//Maximum correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) for a soft
//decision result. No limit is applied since the CRC-8 check rejects most false decodes. Noise injection testing:
//754 correct and 2 false soft decisions, where the false decisions had costs within the range of the correct
//decisions, so a cost limit would reject far more correct decisions than false decisions.
private static final float SOFT_MAX_CORRECTION_COST = Float.MAX_VALUE;
//Soft decision decoder for the Hamming(17,12) rows and even parity columns
private static final SoftProductCodeDecoder SOFT_DECODER = new SoftProductCodeDecoder(
SoftBlockCode.fromCodewordTest(COLUMN_COUNT, word -> Hamming17.getSyndrome(toMessage(word), 0) == 0),
ROW_COUNT, SOFT_MAX_CORRECTION_COST);
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word)
{
BinaryMessage message = new BinaryMessage(COLUMN_COUNT);
for(int i = 0; i < COLUMN_COUNT; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
public BPTC_68_36()
{
super(new Hamming17(), 17, 4);
@@ -42,7 +75,33 @@ public class BPTC_68_36 extends BPTCBase
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
correct(deinterleaved);
return extractPayload(deinterleaved);
}
/**
* Applies soft decision decoding to the soft bits and extracts the 36-bit payload. Can be used when the hard
* decision result fails BPTC error correction or the CRC-8 check.
* @param softBits for the 68 (interleaved) bits of the message where a positive value favors a 1 bit.
* @return decoded message, or null if soft decision decoding doesn't converge.
*/
public CorrectedBinaryMessage extractSoft(float[] softBits)
{
if(softBits == null || softBits.length != LENGTH)
{
return null;
}
CorrectedBinaryMessage decoded = SOFT_DECODER.decode(deinterleave(softBits));
return decoded != null ? extractPayload(decoded) : null;
}
/**
* Extracts the 36-bit payload from the corrected (de-interleaved) message.
* @param deinterleaved message
* @return extracted payload with the corrected bit count transferred.
*/
private static CorrectedBinaryMessage extractPayload(CorrectedBinaryMessage deinterleaved)
{
//Extract the message
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(36);
@@ -59,6 +118,24 @@ public class BPTC_68_36 extends BPTCBase
}
/**
* Deinterleaves the transmitted soft bits.
* @param interleaved soft bits
* @return deinterleaved soft bits.
*/
public static float[] deinterleave(float[] interleaved)
{
float[] deinterleaved = new float[LENGTH];
for(int index = 0; index < 67; index++)
{
deinterleaved[(index * 17) % 67] = interleaved[index];
}
deinterleaved[67] = interleaved[67];
return deinterleaved;
}
/**
* Deinterleaves the transmitted message.
* @param interleaved message

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -59,6 +60,7 @@ public class CACH
private CorrectedBinaryMessage mMessage;
private boolean mValid;
private float[] mPayloadSoftBits;
/**
* Constructs an instance. Note: constructor is private. Use the getCACH() method to extract a CACH from a
@@ -172,6 +174,42 @@ public class CACH
return mMessage.getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
/**
* Soft bits for the 17-bit payload message fragment.
* @return soft bits where a positive value favors a 1 bit, or null if soft bits are not available.
*/
public float[] getPayloadSoftBits()
{
return mPayloadSoftBits;
}
/**
* Utility method to create a CACH from a raw transmitted DMR burst frame. Performs deinterleaving and error
* correction and retains the deinterleaved payload soft bits when the burst soft bits are provided.
*
* @param message containing the raw transmitted DMR burst
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @return constructed CACH message.
*/
public static CACH getCACH(CorrectedBinaryMessage message, float[] softBits)
{
CACH cach = getCACH(message);
if(softBits != null && softBits.length >= CACH_MESSAGE_LENGTH)
{
float[] decodedSoftBits = new float[CACH_MESSAGE_LENGTH];
for(int x = 0; x < CACH_MESSAGE_LENGTH; x++)
{
decodedSoftBits[x] = softBits[INTERLEAVE_MATRIX[x]];
}
cach.mPayloadSoftBits = Arrays.copyOfRange(decodedSoftBits, PAYLOAD_START, PAYLOAD_END);
}
return cach;
}
/**
* Utility method to create a CACH from a raw transmitted DMR burst frame. Performs deinterleaving and error
* correction.

View File

@@ -59,6 +59,22 @@ public class DMRMessageFactory
*/
public IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach, long timestamp,
int timeslot)
{
return create(syncPattern, binaryMessage, null, cach, timestamp, timeslot);
}
/**
* Creates a DMR message wrapper for the binary message. DMR message type is specified by the sync pattern.
* Data messages use soft decision decoding when the soft bits are provided.
* @param syncPattern for the message
* @param binaryMessage containing the raw DMR message burst
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @param timestamp for the burst
* @param timeslot for the burst
* @return DMRMessage instance or null for UNKNOWN sync pattern
*/
public IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
switch(syncPattern)
{
@@ -67,18 +83,18 @@ public class DMRMessageFactory
case MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F:
case DIRECT_VOICE_FRAME_B, DIRECT_VOICE_FRAME_C, DIRECT_VOICE_FRAME_D, DIRECT_VOICE_FRAME_E,
DIRECT_VOICE_FRAME_F:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, timeslot);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, timeslot);
case DIRECT_VOICE_TIMESLOT_1:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 1);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, 1);
case DIRECT_VOICE_TIMESLOT_2:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 2);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, 2);
case BASE_STATION_DATA:
case MOBILE_STATION_DATA:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, timeslot);
case DIRECT_DATA_TIMESLOT_1:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, 1);
case DIRECT_DATA_TIMESLOT_2:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, 2);
case DIRECT_EMPTY_TIMESLOT:
return new EmptyTimeslotPlaceholderMessage(timestamp, 288, Protocol.DMR, timeslot);
case UNKNOWN:
@@ -94,13 +110,14 @@ public class DMRMessageFactory
* Creates a voice message from the raw transmitted DMR burst frame
* @param syncPattern for the frame
* @param message containing the DMR burst
* @param softBits for the DMR burst, or null
* @param cach extracted from the first 24 bits of the message
* @param timestamp the message was received
* @param timeslot for the DMR burst
* @return constructed voice message
*/
private static VoiceMessage createVoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach,
long timestamp, int timeslot)
private static VoiceMessage createVoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message,
float[] softBits, CACH cach, long timestamp, int timeslot)
{
switch(syncPattern)
{
@@ -114,7 +131,7 @@ public class DMRMessageFactory
case DIRECT_VOICE_FRAME_B, DIRECT_VOICE_FRAME_C, DIRECT_VOICE_FRAME_D, DIRECT_VOICE_FRAME_E,
DIRECT_VOICE_FRAME_F:
default:
return new VoiceEMBMessage(syncPattern, message, cach, timestamp, timeslot);
return new VoiceEMBMessage(syncPattern, message, softBits, cach, timestamp, timeslot);
}
}
}

View File

@@ -87,6 +87,23 @@ public class DMRDataMessageFactory
*/
public DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, long timestamp,
int timeslot)
{
return create(pattern, message, null, cach, timestamp, timeslot);
}
/**
* Creates a data message class, using soft decision decoding for the BPTC(196,96) payload when the soft bits are
* provided.
* @param pattern for the DMR burst
* @param message DMR burst as transmitted
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @param cach from the DMR burst
* @param timestamp for the message
* @param timeslot for the message
* @return data message instance
*/
public DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
SlotType slotType = SlotType.getSlotType(message);
@@ -101,181 +118,225 @@ public class DMRDataMessageFactory
}
//Get the BPTC(196,96) protected payload. If the corrected bit count is -1, the payload/message is invalid
CorrectedBinaryMessage payload = getPayload(message);
CorrectedBinaryMessage payload = getPayload(message, softBits);
DataMessage dataMessage = createBPTCMessage(pattern, payload, cach, slotType, timestamp, timeslot);
switch(slotType.getDataType())
//The hard decision decoder can converge on an incorrect codeword (miscorrection) when the error count
//exceeds its correction capability, which then fails the message integrity (ie CRC) check. Soft decision
//decoding is otherwise only applied when hard decision decoding fails, so retry with soft decision
//decoding and use the soft decision message if it passes the integrity check.
if(!dataMessage.isValid() && softBits != null && payload.getCorrectedBitCount() >= 0)
{
case SLOT_IDLE:
DataMessage idle = new IDLEMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
idle.setValid(false);
}
return idle;
case CSBK:
DataMessage csbk = CSBKMessageFactory.create(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
csbk.setValid(false);
}
return csbk;
case USB_DATA:
DataMessage usb = new USBData(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
usb.setValid(false);
}
return usb;
case MBC_ENC_HEADER:
case MBC_HEADER:
DataMessage mbc = new MBCHeader(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbc.setValid(false);
}
return mbc;
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
LCMessage piLinkControl = mLinkControlMessageFactory.createFullEncryption(payload, timestamp,
CorrectedBinaryMessage softPayload = BPTC_196_96.extractSoft(extractSoftBits(softBits));
if(softPayload != null)
{
DataMessage softDataMessage = createBPTCMessage(pattern, softPayload, cach, slotType, timestamp,
timeslot);
DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot, piLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
pi.setValid(false);
}
return pi;
case VOICE_HEADER:
LCMessage vhLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, false);
DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot,
vhLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
voice.setValid(false);
}
return voice;
case DATA_ENC_HEADER:
case DATA_HEADER:
int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC);
boolean valid = crcBitCount < 2 && payload.getCorrectedBitCount() != -1;
DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload);
switch(dpf)
if(softDataMessage.isValid())
{
case CONFIRMED_DATA_PACKET:
ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
cdh.setValid(valid);
return cdh;
case PROPRIETARY_DATA_PACKET:
Vendor vendor = ProprietaryDataHeader.getVendor(payload);
switch(vendor)
{
case MOTOROLA_CAPACITY_PLUS:
case MOTOROLA_CONNECT_PLUS:
ServiceAccessPoint sap = ProprietaryDataHeader.getServiceAccessPoint(payload);
if(sap == ServiceAccessPoint.SAP_1)
{
MNISProprietaryDataHeader mpdh = new MNISProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
mpdh.setValid(valid);
return mpdh;
}
else
{
MotorolaDataEncryptionHeader mprdh = new MotorolaDataEncryptionHeader(pattern,
payload, cach, slotType, timestamp, timeslot);
mprdh.setValid(valid);
return mprdh;
}
case HYTERA_68:
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);
pdh.setValid(valid);
return pdh;
}
case RAW_OR_STATUS_SHORT_DATA:
int appendedBlockCount = ShortDataHeader.getAppendedBlocks(payload);
if(appendedBlockCount == 0)
{
StatusDataHeader sdh = new StatusDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
sdh.setValid(valid);
return sdh;
}
else
{
RawShortDataHeader rsdh = new RawShortDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
rsdh.setValid(valid);
return rsdh;
}
case RESPONSE_PACKET:
ResponseDataHeader rdh = new ResponseDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
rdh.setValid(valid);
return rdh;
case DEFINED_SHORT_DATA:
DefinedShortDataHeader dsdh = new DefinedShortDataHeader(pattern, payload, cach,slotType,
timestamp, timeslot);
dsdh.setValid(valid);
return dsdh;
case UNCONFIRMED_DATA_PACKET:
UnconfirmedDataHeader udh = new UnconfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
udh.setValid(valid);
return udh;
case UNIFIED_DATA_TRANSPORT:
//TODO: this will eventually need its own factory
UDTHeader uh = new UDTHeader(pattern, payload, cach, slotType, timestamp, timeslot);
uh.setValid(valid);
return uh;
default:
DataHeader dh = new DataHeader(pattern, payload, cach, slotType, timestamp, timeslot);
dh.setValid(valid);
return dh;
return softDataMessage;
}
case TLC:
LCMessage tlcLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, true);
DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot, tlcLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
tlc.setValid(false);
}
return tlc;
case RATE_1_OF_2_DATA:
DataMessage data = new DataBlock1_2Rate(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
data.setValid(false);
}
return data;
case MBC_BLOCK:
DataMessage mbcBlock = new MBCContinuationBlock(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbcBlock.setValid(false);
}
return mbcBlock;
case RESERVED_15:
case UNKNOWN:
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
}
return dataMessage;
}
CorrectedBinaryMessage payload = getPayload(message, softBits);
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
/**
* Creates a data message from the BPTC(196,96) protected payload according to the slot type data type.
* @param pattern for the DMR burst
* @param payload extracted from the BPTC(196,96) protected message
* @param cach from the DMR burst
* @param slotType for the DMR burst
* @param timestamp for the message
* @param timeslot for the message
* @return data message instance
*/
private DataMessage createBPTCMessage(DMRSyncPattern pattern, CorrectedBinaryMessage payload, CACH cach,
SlotType slotType, long timestamp, int timeslot)
{
switch(slotType.getDataType())
{
case SLOT_IDLE:
DataMessage idle = new IDLEMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
idle.setValid(false);
}
return idle;
case CSBK:
DataMessage csbk = CSBKMessageFactory.create(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
csbk.setValid(false);
}
return csbk;
case USB_DATA:
DataMessage usb = new USBData(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
usb.setValid(false);
}
return usb;
case MBC_ENC_HEADER:
case MBC_HEADER:
DataMessage mbc = new MBCHeader(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbc.setValid(false);
}
return mbc;
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
LCMessage piLinkControl = mLinkControlMessageFactory.createFullEncryption(payload, timestamp,
timeslot);
DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot, piLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
pi.setValid(false);
}
return pi;
case VOICE_HEADER:
LCMessage vhLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, false);
DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot,
vhLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
voice.setValid(false);
}
return voice;
case DATA_ENC_HEADER:
case DATA_HEADER:
int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC);
boolean valid = crcBitCount < 2 && payload.getCorrectedBitCount() != -1;
DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload);
switch(dpf)
{
case CONFIRMED_DATA_PACKET:
ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
cdh.setValid(valid);
return cdh;
case PROPRIETARY_DATA_PACKET:
Vendor vendor = ProprietaryDataHeader.getVendor(payload);
switch(vendor)
{
case MOTOROLA_CAPACITY_PLUS:
case MOTOROLA_CONNECT_PLUS:
ServiceAccessPoint sap = ProprietaryDataHeader.getServiceAccessPoint(payload);
if(sap == ServiceAccessPoint.SAP_1)
{
MNISProprietaryDataHeader mpdh = new MNISProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
mpdh.setValid(valid);
return mpdh;
}
else
{
MotorolaDataEncryptionHeader mprdh = new MotorolaDataEncryptionHeader(pattern,
payload, cach, slotType, timestamp, timeslot);
mprdh.setValid(valid);
return mprdh;
}
case HYTERA_68:
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);
pdh.setValid(valid);
return pdh;
}
case RAW_OR_STATUS_SHORT_DATA:
int appendedBlockCount = ShortDataHeader.getAppendedBlocks(payload);
if(appendedBlockCount == 0)
{
StatusDataHeader sdh = new StatusDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
sdh.setValid(valid);
return sdh;
}
else
{
RawShortDataHeader rsdh = new RawShortDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
rsdh.setValid(valid);
return rsdh;
}
case RESPONSE_PACKET:
ResponseDataHeader rdh = new ResponseDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
rdh.setValid(valid);
return rdh;
case DEFINED_SHORT_DATA:
DefinedShortDataHeader dsdh = new DefinedShortDataHeader(pattern, payload, cach,slotType,
timestamp, timeslot);
dsdh.setValid(valid);
return dsdh;
case UNCONFIRMED_DATA_PACKET:
UnconfirmedDataHeader udh = new UnconfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
udh.setValid(valid);
return udh;
case UNIFIED_DATA_TRANSPORT:
//TODO: this will eventually need its own factory
UDTHeader uh = new UDTHeader(pattern, payload, cach, slotType, timestamp, timeslot);
uh.setValid(valid);
return uh;
default:
DataHeader dh = new DataHeader(pattern, payload, cach, slotType, timestamp, timeslot);
dh.setValid(valid);
return dh;
}
case TLC:
LCMessage tlcLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, true);
DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot, tlcLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
tlc.setValid(false);
}
return tlc;
case RATE_1_OF_2_DATA:
DataMessage data = new DataBlock1_2Rate(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
data.setValid(false);
}
return data;
case MBC_BLOCK:
DataMessage mbcBlock = new MBCContinuationBlock(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbcBlock.setValid(false);
}
return mbcBlock;
case RESERVED_15:
case UNKNOWN:
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
CorrectedBinaryMessage payload = getPayload(message);
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
@@ -299,11 +360,30 @@ public class DMRDataMessageFactory
* De-scramble, decode and error check a BPTC protected raw message and return a 96-bit error corrected
* payload or null.
* @param message containing a payload
* @param softBits for the message, or null
*/
private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message)
private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message, float[] softBits)
{
CorrectedBinaryMessage extracted = extract(message);
return BPTC_196_96.extract(extracted);
return BPTC_196_96.extract(extracted, extractSoftBits(softBits));
}
/**
* Extracts the 196 soft bits of the message payload from the full DMR burst soft bits
* @param softBits for the DMR burst, or null
* @return extracted soft bits or null
*/
private static float[] extractSoftBits(float[] softBits)
{
if(softBits == null)
{
return null;
}
float[] extracted = new float[196];
System.arraycopy(softBits, 24, extracted, 0, 98);
System.arraycopy(softBits, 190, extracted, 98, 98);
return extracted;
}
/**

View File

@@ -33,6 +33,9 @@ import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
public class FLCAssembler
{
private CorrectedBinaryMessage mAssemblingMessage;
//Soft bits parallel to the assembling message, or null when any fragment doesn't provide soft bits.
private float[] mAssemblingSoftBits;
private int mSoftBitsOffset;
private int mFragmentCount = 0;
private long mTimestamp;
private final int mTimeslot;
@@ -54,6 +57,7 @@ public class FLCAssembler
public void reset()
{
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
}
@@ -62,6 +66,19 @@ public class FLCAssembler
* @return a full link control message or null if the message is not yet fully assembled
*/
public FullLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp)
{
return process(lcss, fragment, null, timestamp);
}
/**
* Processes the full link control fragment
* @param lcss link control start/stop for the fragment
* @param fragment of 32 bits
* @param softFragment soft bits for the fragment where a positive value favors a 1 bit, or null.
* @param timestamp of the fragment
* @return a full link control message or null if the message is not yet fully assembled
*/
public FullLCMessage process(LCSS lcss, BinaryMessage fragment, float[] softFragment, long timestamp)
{
mTimestamp = timestamp;
@@ -71,24 +88,22 @@ public class FLCAssembler
{
case FIRST_FRAGMENT:
message = dispatch();
mAssemblingMessage = new CorrectedBinaryMessage(128);
add(fragment);
start(0);
add(fragment, softFragment);
break;
case CONTINUATION_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(32);
start(32);
}
add(fragment);
add(fragment, softFragment);
break;
case LAST_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(96);
start(96);
}
add(fragment);
add(fragment, softFragment);
message = dispatch();
break;
case SINGLE_FRAGMENT:
@@ -102,6 +117,18 @@ public class FLCAssembler
return message;
}
/**
* Starts assembling a new message
* @param offset for the first fragment
*/
private void start(int offset)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(offset);
mAssemblingSoftBits = new float[128];
mSoftBitsOffset = offset;
}
/**
* Dispatches a fully-received message
*/
@@ -111,10 +138,11 @@ public class FLCAssembler
if(mFragmentCount == 4)
{
message = decode(mAssemblingMessage, mTimestamp, mTimeslot);
message = decode(mAssemblingMessage, mAssemblingSoftBits, mTimestamp, mTimeslot);
}
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
return message;
}
@@ -122,11 +150,11 @@ public class FLCAssembler
/**
* Decodes an assembled FLC message, performs error detection and correction and returns a constructed FLC message
*/
private FullLCMessage decode(BinaryMessage message, long timestamp, int timeslot)
private FullLCMessage decode(BinaryMessage message, float[] softBits, long timestamp, int timeslot)
{
if(message != null)
{
CorrectedBinaryMessage extractedMessage = mBPTC.extract(message);
CorrectedBinaryMessage extractedMessage = mBPTC.extract(message, softBits);
FullLCMessage flco = mMessageFactory.createFull(extractedMessage, timestamp, timeslot, false);
if(extractedMessage.getCorrectedBitCount() < 0)
@@ -141,9 +169,11 @@ public class FLCAssembler
}
/**
* Adds the cach payload to the currently assembling message
* Adds the embedded signalling fragment to the currently assembling message
* @param payload fragment bits
* @param softPayload fragment soft bits, or null
*/
private void add(BinaryMessage payload)
private void add(BinaryMessage payload, float[] softPayload)
{
if(payload == null || payload.size() != 32)
{
@@ -152,6 +182,17 @@ public class FLCAssembler
mFragmentCount++;
if(mAssemblingSoftBits != null && softPayload != null && softPayload.length == 32 &&
mSoftBitsOffset + 32 <= mAssemblingSoftBits.length)
{
System.arraycopy(softPayload, 0, mAssemblingSoftBits, mSoftBitsOffset, 32);
mSoftBitsOffset += 32;
}
else
{
mAssemblingSoftBits = null;
}
try
{
if(mAssemblingMessage == null)

View File

@@ -35,6 +35,9 @@ public class SLCAssembler
{
private final static Logger mLog = LoggerFactory.getLogger(SLCAssembler.class);
private CorrectedBinaryMessage mAssemblingMessage;
//Soft bits parallel to the assembling message, or null when any fragment doesn't provide soft bits.
private float[] mAssemblingSoftBits;
private int mSoftBitsOffset;
private int mFragmentCount = 0;
private long mTimestamp;
private BPTC_68_36 mBPTC = new BPTC_68_36();
@@ -52,6 +55,7 @@ public class SLCAssembler
public void reset()
{
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
mTimestamp = System.currentTimeMillis();
}
@@ -61,6 +65,19 @@ public class SLCAssembler
* @return a short link control message or null if the message is not yet fully assembled
*/
public ShortLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp)
{
return process(lcss, fragment, null, timestamp);
}
/**
* Processes the short link control fragment
* @param lcss link control start/stop for the fragment
* @param fragment of 17 bits
* @param softFragment soft bits for the fragment where a positive value favors a 1 bit, or null.
* @param timestamp of the fragment
* @return a short link control message or null if the message is not yet fully assembled
*/
public ShortLCMessage process(LCSS lcss, BinaryMessage fragment, float[] softFragment, long timestamp)
{
mTimestamp = timestamp;
@@ -70,24 +87,22 @@ public class SLCAssembler
{
case FIRST_FRAGMENT:
message = dispatch();
mAssemblingMessage = new CorrectedBinaryMessage(68);
add(fragment);
start(0);
add(fragment, softFragment);
break;
case CONTINUATION_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(17);
start(17);
}
add(fragment);
add(fragment, softFragment);
break;
case LAST_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(51);
start(51);
}
add(fragment);
add(fragment, softFragment);
message = dispatch();
break;
case SINGLE_FRAGMENT:
@@ -118,34 +133,63 @@ public class SLCAssembler
return LCMessageFactory.createShort(corrected, mTimestamp, 0);
}
/**
* Starts assembling a new message
* @param offset for the first fragment
*/
private void start(int offset)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(offset);
mAssemblingSoftBits = new float[68];
mSoftBitsOffset = offset;
}
private ShortLCMessage dispatch()
{
ShortLCMessage message = null;
if(mFragmentCount == 4)
{
message = decode(mAssemblingMessage, mTimestamp);
message = decode(mAssemblingMessage, mAssemblingSoftBits, mTimestamp);
}
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
return message;
}
/**
* Performs error detection and correction and extracts the message from the BPTC protected interleaved message.
* When the hard decision result fails BPTC error correction or the CRC-8 check and soft bits are provided,
* retries with soft decision decoding and uses the soft decision result if it passes the CRC-8 check.
* @param interleaved message
* @param softBits for the interleaved message, or null.
* @param timestamp for the decoded message.
* @return decoded message.
*/
private ShortLCMessage decode(BinaryMessage interleaved, long timestamp)
private ShortLCMessage decode(BinaryMessage interleaved, float[] softBits, long timestamp)
{
if(interleaved != null)
{
CorrectedBinaryMessage corrected = mBPTC.extract(interleaved);
//Note: slco is timeslot-agnostic, so we use timeslot 0 every time
ShortLCMessage slco = LCMessageFactory.createShort(corrected, timestamp, 0);
slco.setValid(corrected.getCorrectedBitCount() >= 0);
ShortLCMessage slco = create(mBPTC.extract(interleaved), timestamp);
if(!slco.isValid() && softBits != null)
{
CorrectedBinaryMessage softCorrected = mBPTC.extractSoft(softBits);
if(softCorrected != null)
{
ShortLCMessage softSlco = create(softCorrected, timestamp);
if(softSlco.isValid())
{
return softSlco;
}
}
}
return slco;
}
@@ -153,9 +197,26 @@ public class SLCAssembler
}
/**
* Adds the cach payload to the currently assembling message
* Creates a short link control message from the corrected message.
* @param corrected message with the BPTC corrected bit count
* @param timestamp for the message
* @return message
*/
private void add(BinaryMessage payload)
private static ShortLCMessage create(CorrectedBinaryMessage corrected, long timestamp)
{
//Note: slco is timeslot-agnostic, so we use timeslot 0 every time
ShortLCMessage slco = LCMessageFactory.createShort(corrected, timestamp, 0);
//Valid requires both the CRC-8 check (set by the factory) and successful BPTC error correction
slco.setValid(slco.isValid() && corrected.getCorrectedBitCount() >= 0);
return slco;
}
/**
* Adds the cach payload to the currently assembling message
* @param payload fragment bits
* @param softPayload fragment soft bits, or null
*/
private void add(BinaryMessage payload, float[] softPayload)
{
if(payload == null || payload.size() != 17)
{
@@ -164,6 +225,17 @@ public class SLCAssembler
mFragmentCount++;
if(mAssemblingSoftBits != null && softPayload != null && softPayload.length == 17 &&
mSoftBitsOffset + 17 <= mAssemblingSoftBits.length)
{
System.arraycopy(softPayload, 0, mAssemblingSoftBits, mSoftBitsOffset, 17);
mSoftBitsOffset += 17;
}
else
{
mAssemblingSoftBits = null;
}
try
{
if(mAssemblingMessage == null)

View File

@@ -56,6 +56,8 @@ public class EMB
private static final int[] COLOR_CODE = new int[]{0, 1, 2, 3};
private static final int ENCRYPTION_PI = 4;
private static final int[] LINK_CONTROL_START_STOP = new int[]{5, 6};
//Maximum bit errors between the hard decision and the soft decision (maximum likelihood) word for validity.
private static final int SOFT_DECISION_MAX_BIT_ERRORS = 2;
private CorrectedBinaryMessage mMessage;
private boolean mValid = true;
@@ -71,6 +73,68 @@ public class EMB
checkCRC();
}
/**
* Constructs an instance using maximum likelihood soft decision decoding when the soft bits are provided.
*
* @param message with transmitted bits
* @param softBits for the 16 transmitted bits where a positive value favors a 1 bit, or null.
*/
public EMB(CorrectedBinaryMessage message, float[] softBits)
{
mMessage = message;
if(softBits != null && softBits.length == WORD.length)
{
decodeSoft(softBits);
}
else
{
checkCRC();
}
}
/**
* Maximum likelihood soft decision decoding against all valid words. Selects the valid word with the lowest sum
* of soft bit magnitudes for the bit positions where the valid word differs from the hard decision.
* @param softBits for the 16 transmitted bits.
*/
private void decodeSoft(float[] softBits)
{
int word = getMessage().getInt(WORD);
int best = word;
float bestMetric = Float.MAX_VALUE;
for(int validWord: VALID_WORDS)
{
int difference = word ^ validWord;
float metric = 0.0f;
while(difference != 0)
{
int bit = Integer.numberOfTrailingZeros(difference);
//Word bit 15 is message bit 0
metric += Math.abs(softBits[15 - bit]);
difference &= difference - 1;
}
if(metric < bestMetric)
{
bestMetric = metric;
best = validWord;
}
}
int bitErrors = Integer.bitCount(word ^ best);
if(bitErrors > 0)
{
getMessage().setInt(best, WORD);
getMessage().setCorrectedBitCount(bitErrors);
}
mValid = bitErrors <= SOFT_DECISION_MAX_BIT_ERRORS;
}
/**
* Checks the EMB word using the Quadratic Residue(16,7,6) code as detailed in TS 102 361-1 B.3.2.
*

View File

@@ -36,6 +36,7 @@ public class VoiceEMBMessage extends VoiceMessage
private static final int PAYLOAD_END = 172;
private EMB mEMB;
private final float[] mSoftBits;
private EmbeddedParameters mEmbeddedParameters;
/**
@@ -47,8 +48,23 @@ public class VoiceEMBMessage extends VoiceMessage
* @param message containing 288-bit DMR message with preliminary bit corrections indicated.
*/
public VoiceEMBMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, int timeslot)
{
this(syncPattern, message, null, cach, timestamp, timeslot);
}
/**
* Constructs an instance with soft bits to use for soft decision decoding of the EMB and the embedded signalling
* full link control fragment.
*
* @param syncPattern
* @param message containing 288-bit DMR message with preliminary bit corrections indicated.
* @param softBits for the 288-bit DMR message where a positive value favors a 1 bit, or null.
*/
public VoiceEMBMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
super(syncPattern, message, cach, timestamp, timeslot);
mSoftBits = softBits;
}
@Override
@@ -90,7 +106,19 @@ public class VoiceEMBMessage extends VoiceMessage
segment.set(x, getMessage().get(EMB[x]));
}
mEMB = new EMB(segment);
float[] softBits = null;
if(mSoftBits != null)
{
softBits = new float[EMB.length];
for(int x = 0; x < EMB.length; x++)
{
softBits[x] = mSoftBits[EMB[x]];
}
}
mEMB = new EMB(segment, softBits);
}
return mEMB;
@@ -104,6 +132,22 @@ public class VoiceEMBMessage extends VoiceMessage
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
/**
* Soft bits for the embedded signalling full link control fragment
* @return soft bits or null if soft bits are not available.
*/
public float[] getFLCFragmentSoftBits()
{
if(mSoftBits == null)
{
return null;
}
float[] softBits = new float[PAYLOAD_END - PAYLOAD_START];
System.arraycopy(mSoftBits, PAYLOAD_START, softBits, 0, softBits.length);
return softBits;
}
/**
* Optional embedded parameters for this voice super-frame.
* @return encryption parameters or null.

View File

@@ -19,14 +19,23 @@
package io.github.dsheirer.module.decode.dmr.sync;
import io.github.dsheirer.dsp.c4fm.DibitSyncDetector;
import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector;
import io.github.dsheirer.dsp.c4fm.ReferenceSyncWaveform;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
/**
* DMR Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream.
*
* Implements the C4FM sync detector interface where the sync pattern accessor methods return the values for the sync
* pattern that produced the best correlation score from the most recent call to process().
*/
public abstract class DMRSoftSyncDetector extends DMRSyncDetector
public abstract class DMRSoftSyncDetector extends DMRSyncDetector implements ISoftC4FMSyncDetector
{
private static final int SYNC_DIBIT_LENGTH = 24;
protected static final float MAX_POSITIVE = Dibit.D01_PLUS_3.getIdealPhase();
protected static final float MAX_NEGATIVE = Dibit.D11_MINUS_3.getIdealPhase();
protected static final float[] BASE_DATA = DMRSyncPattern.BASE_STATION_DATA.toSymbols();
@@ -40,9 +49,47 @@ public abstract class DMRSoftSyncDetector extends DMRSyncDetector
protected float[] mSymbols = new float[48]; //2x longer than 48-bit/24-dibit DMR sync patterns.
protected int mSymbolPointer = 0;
//Per-instance sync pattern detectors provide pattern dibits, symbols and (cached) reference waveforms
private final Map<DMRSyncPattern,DibitSyncDetector> mPatternDetectors = new EnumMap<>(DMRSyncPattern.class);
public DMRSoftSyncDetector()
{
for(DMRSyncPattern pattern: DMRSyncPattern.SYNC_PATTERNS)
{
mPatternDetectors.put(pattern, new DibitSyncDetector(pattern.getPattern(), SYNC_DIBIT_LENGTH));
}
}
/**
* Sync pattern detector for the currently detected sync pattern.
*/
private DibitSyncDetector getDetectedPatternDetector()
{
return mPatternDetectors.get(mDetectedPattern);
}
@Override
public float[] getPatternSymbols()
{
return getDetectedPatternDetector().getPatternSymbols();
}
@Override
public Dibit[] getPatternDibits()
{
return getDetectedPatternDetector().getPatternDibits();
}
@Override
public int getDibitLength()
{
return SYNC_DIBIT_LENGTH;
}
@Override
public ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol)
{
return getDetectedPatternDetector().getReferenceWaveform(samplesPerSymbol);
}
/**
@@ -74,23 +121,15 @@ public abstract class DMRSoftSyncDetector extends DMRSyncDetector
* symbols that include this recent soft symbol.
*
* @param dibitSymbol to process that represents the soft demodulated symbol value in phase angle/radians.
* @return best correlation score where the best detected pattern is available at getDetectedPattern().
*/
public void process(float dibitSymbol)
@Override
public float process(float dibitSymbol)
{
mSymbols[mSymbolPointer] = dibitSymbol;
mSymbols[mSymbolPointer + 24] = dibitSymbol;
mSymbolPointer++;
mSymbolPointer %= 24;
}
/**
* Process the symbol and calculate the sync detection score.
* @param symbol to process
* @return sync detection score.
*/
public float processAndCalculate(float symbol)
{
process(symbol);
return calculate();
}
}

View File

@@ -50,7 +50,6 @@ public class NXDNMessageFramer extends C4FMMessageFramer
//Soft symbols (radians) parallel to the dibit buffer, or NaN when the soft symbol wasn't provided
private final float[] mSoftSymbolBuffer = new float[192];
private float mCurrentSoftSymbol = Float.NaN;
private static final float SOFT_SYMBOL_SCALE = (float)(4.0 / Math.PI); //Radians to symbol units (+/-1, +/-3)
private final LICHTracker mLICHTracker;
private final Listener<IMessage> mMessageListener;
private final NXDNHardSyncDetector mHardSyncDetector = new NXDNHardSyncDetector();
@@ -105,24 +104,6 @@ public class NXDNMessageFramer extends C4FMMessageFramer
}
}
/**
* Converts the soft symbol into a pair of soft bit values using max-log likelihood ratios for the Gray mapped
* 4-level symbol (D01=+3, D00=+1, D10=-1, D11=-3), where a positive soft bit value favors a 1 bit.
* @param softSymbol in radians
* @param softBits array to receive the two soft bit values
* @param index for the first bit (sign bit), followed by the second (magnitude) bit
*/
private static void setSoftBits(float softSymbol, float[] softBits, int index)
{
float u = softSymbol * SOFT_SYMBOL_SCALE;
float magnitude = Math.abs(u);
//First bit is 1 for negative symbols
float favorZero = magnitude <= 2.0f ? 4.0f * u : 8.0f * u - 8.0f * Math.signum(u);
softBits[index] = -favorZero;
//Second bit is 1 for the outer (+/-3) symbols
softBits[index + 1] = 4.0f * (magnitude - 2.0f);
}
/**
* Dispatches a currently assembling message once it's complete.
* @return true if any of the messages from the frame are valid.

View File

@@ -60,11 +60,11 @@ public class DmrSoftSyncDetectorTest
for(Dibit dibit: pattern.toDibits())
{
symbol = toSymbol(dibit);
scalar.processAndCalculate(symbol);
vector64.processAndCalculate(symbol);
vector128.processAndCalculate(symbol);
vector256.processAndCalculate(symbol);
vector512.processAndCalculate(symbol);
scalar.process(symbol);
vector64.process(symbol);
vector128.process(symbol);
vector256.process(symbol);
vector512.process(symbol);
}
assertSame(scalar.getDetectedPattern(), pattern, "Scalar Detector Did Not Correctly Detect Pattern: " + pattern);

View File

@@ -0,0 +1,399 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Golay24;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Utility for creating DMR data bursts and framing them for unit tests.
*/
public class DMRBurstTestUtility
{
public static final int BURST_DIBITS = 144;
public static final int DELAY_LINE_DIBITS = 90; //CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
public static final DMRSyncPattern SYNC_PATTERN = DMRSyncPattern.BASE_STATION_DATA;
public static final int CSBK_CRC_MASK = 0xA5A5;
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(15,
word -> Hamming15.getSyndrome(toMessage(word, 15), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(13,
word -> Hamming13.getSyndrome(toMessage(word, 13), new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) == 0);
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Finds the systematic codeword whose information bits (the low bits) match the information value.
*/
private static int encode(SoftBlockCode code, int information, int informationLength)
{
int mask = (1 << informationLength) - 1;
for(int codeword: code.getCodewords())
{
if((codeword & mask) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a random valid BPTC(196,96) codeword in transmitted (interleaved) bit order.
*/
public static CorrectedBinaryMessage createInterleavedBPTC(Random random)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196);
for(int row = 0; row < 9; row++)
{
int information = random.nextInt(1 << 11);
for(int column = 0; column < 11; column++)
{
if((information & (1 << column)) != 0)
{
deinterleaved.set(BPTC_196_96.getIndex(column, row));
}
}
}
return encodeAndInterleave(deinterleaved);
}
/**
* Creates a valid BPTC(196,96) codeword in transmitted (interleaved) bit order that carries the 96-bit payload,
* with the reserved bits set to zero.
* @param payload of 96 bits
*/
public static CorrectedBinaryMessage createInterleavedBPTC(BinaryMessage payload)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196);
//Payload bits occupy the row information columns in order, starting at index 4 (see BPTC_196_96.extract())
int payloadIndex = 0;
for(int index = 4; index < BPTC_196_96.MAX_ORIGINAL_INDEX && payloadIndex < 96; index++)
{
if((index % BPTC_196_96.COLUMN_COUNT) >= 1 && (index % BPTC_196_96.COLUMN_COUNT) <= 11)
{
if(payload.get(payloadIndex))
{
deinterleaved.set(index);
}
payloadIndex++;
}
}
return encodeAndInterleave(deinterleaved);
}
/**
* Encodes the row and column parity for the information bits (rows 0-8, columns 0-10) of the de-interleaved
* message and interleaves the result.
*/
private static CorrectedBinaryMessage encodeAndInterleave(CorrectedBinaryMessage deinterleaved)
{
//Information rows 0-8, each encoded with the row code
for(int row = 0; row < 9; row++)
{
int information = 0;
for(int column = 0; column < 11; column++)
{
if(deinterleaved.get(BPTC_196_96.getIndex(column, row)))
{
information |= (1 << column);
}
}
int rowWord = encode(ROW_CODE, information, 11);
for(int column = 11; column < 15; column++)
{
if((rowWord & (1 << column)) != 0)
{
deinterleaved.set(BPTC_196_96.getIndex(column, row));
}
}
}
//Parity rows 9-12 from the column code
for(int column = 0; column < 15; column++)
{
int information = 0;
for(int row = 0; row < 9; row++)
{
if(deinterleaved.get(BPTC_196_96.COLUMN_INDEXES[column][row]))
{
information |= (1 << row);
}
}
int columnWord = encode(COLUMN_CODE, information, 9);
for(int row = 9; row < 13; row++)
{
if((columnWord & (1 << row)) != 0)
{
deinterleaved.set(BPTC_196_96.COLUMN_INDEXES[column][row]);
}
}
}
//De-interleave maps deinterleaved[x] = interleaved[DEINTERLEAVE[x]], so interleave is the inverse
CorrectedBinaryMessage interleaved = new CorrectedBinaryMessage(196);
for(int x = 0; x < 196; x++)
{
if(deinterleaved.get(x))
{
interleaved.set(BPTC_196_96.BPTC_DEINTERLEAVE[x]);
}
}
return interleaved;
}
/**
* Creates a random 96-bit CSBK payload with a valid CRC-CCITT (80 message bits and 16 CRC bits).
*/
public static BinaryMessage createCSBKPayload(Random random)
{
BinaryMessage payload = new BinaryMessage(96);
int crc = CSBK_CRC_MASK;
for(int x = 0; x < 80; x++)
{
if(random.nextBoolean())
{
payload.set(x);
crc ^= CRCDMR.CCITT_80_CHECKSUMS[x];
}
}
for(int x = 0; x < 16; x++)
{
if((crc & (1 << (15 - x))) != 0)
{
payload.set(80 + x);
}
}
return payload;
}
/**
* Creates the 20 transmitted slot type bits: Golay(24,12) shortened by 4 leading zero bits, where the 24-bit
* message has the color code at bits 4-7, the data type at 8-11, the 11-bit checksum at 12-22 and even parity at 23.
*/
public static boolean[] createSlotType(int colorCode, DataType dataType)
{
boolean[] golay = new boolean[24];
int information = (colorCode << 4) | dataType.getValue();
int checksum = 0;
for(int bit = 0; bit < 8; bit++)
{
if((information & (1 << (7 - bit))) != 0)
{
golay[4 + bit] = true;
checksum ^= Golay24.CHECKSUMS[4 + bit];
}
}
int ones = Integer.bitCount(information);
for(int bit = 0; bit < 11; bit++)
{
if((checksum & (1 << (10 - bit))) != 0)
{
golay[12 + bit] = true;
ones++;
}
}
golay[23] = ones % 2 != 0;
boolean[] transmitted = new boolean[20];
System.arraycopy(golay, 4, transmitted, 0, 20);
return transmitted;
}
/**
* Creates a 288-bit DMR base station data burst: CACH (0-23), BPTC payload part 1 (24-121), slot type part 1
* (122-131), sync (132-179), slot type part 2 (180-189) and BPTC payload part 2 (190-287).
*/
public static boolean[] createBurst(CorrectedBinaryMessage bptc, int colorCode, DataType dataType)
{
boolean[] burst = new boolean[288];
boolean[] slotType = createSlotType(colorCode, dataType);
for(int x = 0; x < 98; x++)
{
burst[24 + x] = bptc.get(x);
burst[190 + x] = bptc.get(98 + x);
}
for(int x = 0; x < 10; x++)
{
burst[122 + x] = slotType[x];
burst[180 + x] = slotType[10 + x];
}
Dibit[] sync = SYNC_PATTERN.toDibits();
for(int x = 0; x < sync.length; x++)
{
burst[132 + 2 * x] = sync[x].getBit1();
burst[133 + 2 * x] = sync[x].getBit2();
}
return burst;
}
/**
* Converts the burst bits into ideal soft symbols.
*/
public static float[] toSoftSymbols(boolean[] burst)
{
float[] softSymbols = new float[BURST_DIBITS];
for(int x = 0; x < BURST_DIBITS; x++)
{
int value = (burst[2 * x] ? 2 : 0) + (burst[2 * x + 1] ? 1 : 0);
softSymbols[x] = Dibit.fromValue(value).getIdealPhase();
}
return softSymbols;
}
/**
* BPTC payload dibit indexes in the burst, excluding the CACH, slot type and sync dibits.
*/
public static List<Integer> getPayloadDibits()
{
List<Integer> payloadDibits = new ArrayList<>();
for(int x = 12; x < 61; x++)
{
payloadDibits.add(x);
}
for(int x = 95; x < BURST_DIBITS; x++)
{
payloadDibits.add(x);
}
return payloadDibits;
}
/**
* Moves the soft symbol just across the outer/inner (PI/2) decision boundary, flipping the low reliability
* magnitude bit of the symbol decision.
* @param softSymbols to modify
* @param burst bits to modify to match the new symbol decision
* @param dibit index of the symbol to modify
* @param margin distance from the decision boundary
*/
public static void addLowReliabilityError(float[] softSymbols, boolean[] burst, int dibit, float margin)
{
float sign = Math.signum(softSymbols[dibit]);
boolean outer = Math.abs(softSymbols[dibit]) > Math.PI / 2;
softSymbols[dibit] = sign * ((float)(Math.PI / 2) + (outer ? -margin : margin));
burst[2 * dibit + 1] = !burst[2 * dibit + 1];
}
/**
* Extracts the transmitted BPTC(196,96) bits from the burst.
*/
public static CorrectedBinaryMessage getBPTC(boolean[] burst)
{
CorrectedBinaryMessage bptc = new CorrectedBinaryMessage(196);
for(int x = 0; x < 98; x++)
{
bptc.set(x, burst[24 + x]);
bptc.set(98 + x, burst[190 + x]);
}
return bptc;
}
/**
* Feeds the burst soft symbols to the framer via the C4FM input, signals the sync detection on the final sync
* dibit, and then flushes the burst through the delay line with filler symbols.
* @return messages dispatched by the framer.
*/
public static List<IMessage> frame(float[] softSymbols)
{
List<IMessage> messages = new ArrayList<>();
DMRMessageFramer framer = new DMRMessageFramer(new DMRCrcMaskManager(false));
framer.setListener(messages::add);
framer.start();
//The sync pattern ends at dibit 89 (CACH 12 + prefix 54 + sync 24)
for(int x = 0; x < DELAY_LINE_DIBITS; x++)
{
framer.process(Dibit.fromSample(softSymbols[x]), softSymbols[x]);
}
framer.setSyncPattern(SYNC_PATTERN);
framer.syncDetected();
//Remaining burst dibits, followed by filler dibits to eject the full burst from the delay line
for(int x = DELAY_LINE_DIBITS; x < BURST_DIBITS + DELAY_LINE_DIBITS; x++)
{
float softSymbol = x < BURST_DIBITS ? softSymbols[x] : Dibit.D00_PLUS_1.getIdealPhase();
framer.process(Dibit.fromSample(softSymbol), softSymbol);
}
return messages;
}
}

View File

@@ -0,0 +1,160 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.DMRMessageFactory;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.SYNC_PATTERN;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.addLowReliabilityError;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createBurst;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createCSBKPayload;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createInterleavedBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.frame;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getPayloadDibits;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.toSoftSymbols;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR data message factory.
*/
public class DMRDataMessageFactoryTest
{
private static final int COLOR_CODE = 3;
private static final int ERROR_COUNT = 12;
//Soft symbol distance from the +/-3 and +/-1 decision boundary (PI/2) for low reliability symbol errors.
private static final float LOW_RELIABILITY_MARGIN = 0.05f;
/**
* Finds the CSBK message in the list of messages
*/
private static CSBKMessage getCSBK(List<IMessage> messages)
{
CSBKMessage csbk = null;
for(IMessage message: messages)
{
if(message instanceof CSBKMessage csbkMessage)
{
csbk = csbkMessage;
}
}
assertNotNull(csbk, "Expected a CSBK message: " + messages);
return csbk;
}
/**
* Test: an error free CSBK burst produces a valid CSBK message with the expected payload.
*/
@Test
void testValidCSBK()
{
Random random = new Random(1);
CorrectedBinaryMessage bptc = createInterleavedBPTC(createCSBKPayload(random));
CSBKMessage csbk = getCSBK(frame(toSoftSymbols(createBurst(bptc, COLOR_CODE, DataType.CSBK))));
assertTrue(csbk.isValid());
assertEquals(BPTC_196_96.extract(bptc).toString(), csbk.getMessage().toString());
}
/**
* Test: when the hard decision decoder converges on an incorrect codeword (miscorrection) and the CSBK fails the
* CRC check, the factory retries with soft decision decoding, which recovers the transmitted CSBK.
*
* Note: some miscorrections differ from the transmitted codeword by a single CRC protected bit, which the
* CSBK CRC check corrects. Those trials are verified to have the correct CRC protected content.
*/
@Test
void testSoftRetryAfterHardMiscorrection()
{
List<Integer> payloadDibits = getPayloadDibits();
DMRMessageFactory factory = new DMRMessageFactory(new DMRCrcMaskManager(false));
int crcFailures = 0;
for(int trial = 0; trial < 3000 && crcFailures < 5; trial++)
{
Random random = new Random(1000 + trial);
CorrectedBinaryMessage bptc = createInterleavedBPTC(createCSBKPayload(random));
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
boolean[] burst = createBurst(bptc, COLOR_CODE, DataType.CSBK);
float[] softSymbols = toSoftSymbols(burst);
Set<Integer> errors = new HashSet<>();
while(errors.size() < ERROR_COUNT)
{
errors.add(payloadDibits.get(random.nextInt(payloadDibits.size())));
}
for(int error: errors)
{
addLowReliabilityError(softSymbols, burst, error, LOW_RELIABILITY_MARGIN);
}
//Only evaluate trials where the hard decision decoder reports success with an incorrect codeword
CorrectedBinaryMessage hard = BPTC_196_96.extract(getBPTC(burst));
if(hard.getCorrectedBitCount() < 0 || hard.toString().equals(expected.toString()))
{
continue;
}
//Without soft bits, the factory creates the CSBK from the miscorrected payload
CorrectedBinaryMessage burstMessage = new CorrectedBinaryMessage(288);
for(int x = 0; x < 288; x++)
{
burstMessage.set(x, burst[x]);
}
IMessage hardMessage = factory.create(SYNC_PATTERN, burstMessage, null, CACH.getCACH(burstMessage), 0L, 1);
assertTrue(hardMessage instanceof CSBKMessage, "Trial " + trial);
if(hardMessage.isValid())
{
//The CRC check corrected the miscorrected payload bit, so the CRC protected content must be correct
assertEquals(expected.getSubMessage(0, 96).toString(),
((CSBKMessage)hardMessage).getMessage().getSubMessage(0, 96).toString(), "Trial " + trial);
continue;
}
crcFailures++;
//With soft symbols, the factory retries with soft decision decoding and recovers the transmitted CSBK
CSBKMessage csbk = getCSBK(frame(softSymbols));
assertTrue(csbk.isValid(), "Trial " + trial);
assertEquals(expected.toString(), csbk.getMessage().toString(), "Trial " + trial);
}
assertEquals(5, crcFailures, "Expected to find 5 hard decision miscorrections that fail the CRC check");
}
}

View File

@@ -0,0 +1,144 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.addLowReliabilityError;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createBurst;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.createInterleavedBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.frame;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getBPTC;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.getPayloadDibits;
import static io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility.toSoftSymbols;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR message framer C4FM symbol stream input, where the framer maintains the burst delay lines
* for the dibits and soft symbols and the sync detection is signalled on the final dibit of the sync pattern.
*/
public class DMRMessageFramerTest
{
private static final int COLOR_CODE = 5;
//Soft symbol distance from the +/-3 and +/-1 decision boundary (PI/2) for low reliability symbol errors.
private static final float LOW_RELIABILITY_MARGIN = 0.05f;
/**
* Finds the IDLE message dispatched by the framer
*/
private static IDLEMessage getIdleMessage(List<IMessage> messages)
{
IDLEMessage idle = null;
for(IMessage message: messages)
{
if(message instanceof IDLEMessage idleMessage)
{
idle = idleMessage;
}
}
assertNotNull(idle, "Framer did not dispatch an IDLE message: " + messages);
return idle;
}
/**
* Test: a burst fed through the C4FM input with the sync detection signalled on the final sync dibit is framed
* with the correct alignment so that the slot type and BPTC payload decode without errors.
*/
@Test
void testBurstAlignment()
{
Random random = new Random(1);
CorrectedBinaryMessage bptc = createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
assertEquals(0, expected.getCorrectedBitCount());
IDLEMessage idle = getIdleMessage(frame(toSoftSymbols(createBurst(bptc, COLOR_CODE, DataType.SLOT_IDLE))));
assertTrue(idle.isValid());
assertEquals(COLOR_CODE, idle.getSlotType().getColorCode());
assertEquals(DataType.SLOT_IDLE, idle.getSlotType().getDataType());
assertEquals(expected.toString(), idle.getMessage().toString());
assertEquals(0, idle.getMessage().getCorrectedBitCount());
}
/**
* Test: low reliability symbol errors in the BPTC payload that exceed the hard decision decoder capability are
* corrected by soft decision decoding, which requires the soft symbols to remain aligned with the dibits through
* the framer delay lines.
*
* Note: for IDLE messages (no CRC), soft decision decoding is only applied when the hard decision decoder fails,
* so trials where the hard decision decoder reports success (correctly, or with a miscorrection) don't exercise
* the soft symbols and are not evaluated.
*/
@Test
void testSoftSymbolAlignment()
{
List<Integer> payloadDibits = getPayloadDibits();
int hardFailures = 0;
for(int trial = 0; trial < 40; trial++)
{
Random random = new Random(100 + trial);
CorrectedBinaryMessage bptc = createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
boolean[] burst = createBurst(bptc, COLOR_CODE, DataType.SLOT_IDLE);
float[] softSymbols = toSoftSymbols(burst);
//Add 10 low reliability payload symbol errors
Set<Integer> errors = new HashSet<>();
while(errors.size() < 10)
{
errors.add(payloadDibits.get(random.nextInt(payloadDibits.size())));
}
for(int error: errors)
{
addLowReliabilityError(softSymbols, burst, error, LOW_RELIABILITY_MARGIN);
}
//Only evaluate trials where the hard decision decoder fails
if(BPTC_196_96.extract(getBPTC(burst)).getCorrectedBitCount() >= 0)
{
continue;
}
hardFailures++;
IDLEMessage idle = getIdleMessage(frame(softSymbols));
assertTrue(idle.isValid(), "Trial " + trial);
assertEquals(expected.toString(), idle.getMessage().toString(), "Trial " + trial);
}
//Sanity check that the soft decision decoder was exercised
assertTrue(hardFailures >= 5, "Expected at least 5 hard decision failures, but found " + hardFailures);
}
}

View File

@@ -0,0 +1,238 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming16;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.module.decode.dmr.message.voice.EMB;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for DMR soft decision decoding of the embedded signalling BPTC(128,77) product code and the EMB.
*/
public class BPTC_128_77SoftTest
{
private static final int[] WORD = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(16, word -> {
BinaryMessage message = new BinaryMessage(16);
for(int i = 0; i < 16; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return Hamming16.getSyndrome(message, 0) == 0;
});
/**
* Creates a random valid (de-interleaved) BPTC(128,77) product codeword: 7 random Hamming(16,11,4) rows followed
* by the column parity row.
*/
private static CorrectedBinaryMessage createCodeword(Random random)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(128);
int[] codewords = ROW_CODE.getCodewords();
int parityRow = 0;
for(int row = 0; row < 7; row++)
{
int rowWord = codewords[random.nextInt(codewords.length)];
parityRow ^= rowWord;
for(int column = 0; column < 16; column++)
{
if((rowWord & (1 << column)) != 0)
{
message.set(row * 16 + column);
}
}
}
for(int column = 0; column < 16; column++)
{
if((parityRow & (1 << column)) != 0)
{
message.set(7 * 16 + column);
}
}
return message;
}
/**
* Test: the row code is Hamming(16,11) with 2048 codewords and the generated product codewords are valid for the
* hard decision decoder.
*/
@Test
void testValidCodeword()
{
assertEquals(2048, ROW_CODE.getCodewords().length);
BPTC_128_77 bptc = new BPTC_128_77();
CorrectedBinaryMessage codeword = createCodeword(new Random(1));
assertTrue(bptc.getRowErrors(codeword).isEmpty());
assertTrue(bptc.getColumnErrors(codeword).isEmpty());
float[] softBits = new float[128];
for(int x = 0; x < 128; x++)
{
softBits[x] = codeword.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage decoded = bptc.decodeSoft(softBits);
assertNotNull(decoded);
assertEquals(codeword, decoded);
assertEquals(0, decoded.getCorrectedBitCount());
}
/**
* Test: soft decoding corrects 6 low reliability bit errors placed randomly across the product codeword.
*/
@Test
void testSoftDecodeRandomErrors()
{
Random random = new Random(2);
BPTC_128_77 bptc = new BPTC_128_77();
int hardFailures = 0;
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage codeword = createCodeword(random);
float[] softBits = new float[128];
for(int x = 0; x < 128; x++)
{
float magnitude = 0.6f + 0.8f * random.nextFloat();
softBits[x] = codeword.get(x) ? magnitude : -magnitude;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < 6)
{
errors.add(random.nextInt(128));
}
CorrectedBinaryMessage hard = new CorrectedBinaryMessage(codeword);
for(int error: errors)
{
softBits[error] = -0.3f * random.nextFloat() * Math.signum(softBits[error]);
hard.flip(error);
}
if(!bptc.correct(hard) || !hard.equals(codeword))
{
hardFailures++;
}
CorrectedBinaryMessage decoded = bptc.decodeSoft(softBits);
assertNotNull(decoded, "Soft decode failed on trial " + trial);
assertEquals(codeword, decoded, "Soft decode miscorrected on trial " + trial);
assertEquals(6, decoded.getCorrectedBitCount());
}
//Sanity check that this scenario exceeds the hard decision decoder capability
assertTrue(hardFailures > 0, "Expected hard decision failures");
}
/**
* Test: EMB soft decoding corrects 4 low reliability bit errors (beyond the hard decoder's 3-bit capability) and
* flags the result invalid since it exceeds the soft decision validity threshold. Also verifies that a single
* low reliability bit error is corrected and valid.
*/
@Test
void testEMBSoftDecode()
{
Random random = new Random(3);
CorrectedBinaryMessage reference = new CorrectedBinaryMessage(16);
for(int trial = 0; trial < 100; trial++)
{
//Find a valid word by hard decoding a random word with no more than 1 bit error after correction
int word;
EMB referenceEMB;
do
{
word = random.nextInt(1 << 16);
reference.setInt(word, WORD);
referenceEMB = new EMB(new CorrectedBinaryMessage(reference));
}
while(!referenceEMB.isValid());
//Recover the corrected valid word
CorrectedBinaryMessage corrected = new CorrectedBinaryMessage(reference);
new EMB(corrected);
int validWord = corrected.getInt(0, 15);
for(int errorCount: new int[]{1, 4})
{
float[] softBits = new float[16];
CorrectedBinaryMessage received = new CorrectedBinaryMessage(16);
received.setInt(validWord, WORD);
for(int x = 0; x < 16; x++)
{
softBits[x] = received.get(x) ? 1.0f : -1.0f;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < errorCount)
{
errors.add(random.nextInt(16));
}
for(int error: errors)
{
received.flip(error);
softBits[error] = -0.2f * softBits[error];
}
EMB emb = new EMB(received, softBits);
assertEquals(validWord, received.getInt(0, 15), "Trial " + trial + " errors " + errorCount);
if(errorCount == 1)
{
assertTrue(emb.isValid());
}
else
{
assertFalse(emb.isValid());
}
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for DMR soft decision decoding of the BPTC(196,96) product code.
*/
public class BPTC_196_96SoftTest
{
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(15,
word -> Hamming15.getSyndrome(toMessage(word, 15), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(13,
word -> Hamming13.getSyndrome(toMessage(word, 13), new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) == 0);
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Finds the systematic codeword whose information bits (the low bits) match the information value.
*/
private static int encode(SoftBlockCode code, int information, int informationLength)
{
int mask = (1 << informationLength) - 1;
for(int codeword: code.getCodewords())
{
if((codeword & mask) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a random valid (de-interleaved) BPTC(196,96) product codeword.
*/
private static CorrectedBinaryMessage createCodeword(Random random)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(196);
//Information rows 0-8, each encoded with the row code
for(int row = 0; row < 9; row++)
{
int rowWord = encode(ROW_CODE, random.nextInt(1 << 11), 11);
for(int x = 0; x < 15; x++)
{
if((rowWord & (1 << x)) != 0)
{
message.set(BPTC_196_96.getIndex(x, row));
}
}
}
//Parity rows 9-12 from the column code
for(int column = 0; column < 15; column++)
{
int information = 0;
for(int row = 0; row < 9; row++)
{
if(message.get(BPTC_196_96.COLUMN_INDEXES[column][row]))
{
information |= (1 << row);
}
}
int columnWord = encode(COLUMN_CODE, information, 9);
for(int row = 9; row < 13; row++)
{
if((columnWord & (1 << row)) != 0)
{
message.set(BPTC_196_96.COLUMN_INDEXES[column][row]);
}
}
}
return message;
}
/**
* Test: valid product codewords have no row or column errors and soft decoding of an error free codeword
* returns the codeword.
*/
@Test
void testValidCodeword()
{
Random random = new Random(2);
CorrectedBinaryMessage codeword = createCodeword(random);
CorrectedBinaryMessage copy = new CorrectedBinaryMessage(codeword);
assertTrue(BPTC_196_96.correct(copy, true));
assertEquals(codeword, copy);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
softBits[x] = codeword.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage decoded = BPTC_196_96.decodeSoft(softBits);
assertNotNull(decoded);
assertEquals(codeword, decoded);
assertEquals(0, decoded.getCorrectedBitCount());
}
/**
* Test: soft decoding corrects 10 low reliability bit errors placed randomly across the product codeword.
*/
@Test
void testSoftDecodeRandomErrors()
{
Random random = new Random(3);
int hardFailures = 0;
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage codeword = createCodeword(random);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
float magnitude = 0.6f + 0.8f * random.nextFloat();
softBits[x] = codeword.get(x) ? magnitude : -magnitude;
}
Set<Integer> errors = new HashSet<>();
while(errors.size() < 10)
{
errors.add(1 + random.nextInt(195));
}
CorrectedBinaryMessage hard = new CorrectedBinaryMessage(codeword);
for(int error: errors)
{
softBits[error] = -0.3f * random.nextFloat() * Math.signum(softBits[error]);
hard.flip(error);
}
if(!BPTC_196_96.correct(hard, true) || !hard.equals(codeword))
{
hardFailures++;
}
CorrectedBinaryMessage decoded = BPTC_196_96.decodeSoft(softBits);
assertNotNull(decoded, "Soft decode failed on trial " + trial);
assertEquals(codeword, decoded, "Soft decode miscorrected on trial " + trial);
assertEquals(10, decoded.getCorrectedBitCount());
}
//Sanity check that this scenario exceeds the hard decision decoder capability
assertTrue(hardFailures > 0, "Expected hard decision failures");
}
}

View File

@@ -0,0 +1,115 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.bptc;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRBurstTestUtility;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the DMR BPTC(196,96) hard decision decoder.
*/
public class BPTC_196_96Test
{
/**
* Test: when the hard decision decoder recovers the transmitted codeword, the corrected bit count equals the
* number of bit errors. The turbo decoder's incremental accounting previously counted some corrections twice
* (ie reporting 13-14 corrected bits for 10 bit errors).
*/
@Test
void testCorrectedBitCount()
{
Random random = new Random(1);
int[] correctDecodes = new int[9];
for(int errorCount = 1; errorCount <= 8; errorCount++)
{
for(int trial = 0; trial < 200; trial++)
{
CorrectedBinaryMessage bptc = DMRBurstTestUtility.createInterleavedBPTC(random);
CorrectedBinaryMessage expected = BPTC_196_96.extract(bptc);
CorrectedBinaryMessage received = new CorrectedBinaryMessage(bptc);
//Bit 0 is a pad bit that isn't protected by the row or column codes
Set<Integer> errors = new HashSet<>();
while(errors.size() < errorCount)
{
errors.add(1 + random.nextInt(195));
}
for(int error: errors)
{
received.flip(error);
}
CorrectedBinaryMessage decoded = BPTC_196_96.extract(received);
if(decoded.getCorrectedBitCount() >= 0 && decoded.toString().equals(expected.toString()))
{
correctDecodes[errorCount]++;
assertEquals(errorCount, decoded.getCorrectedBitCount(), "Errors " + errorCount + " trial " + trial);
}
}
}
//Up to 2 bit errors are always corrected
assertEquals(200, correctDecodes[1]);
assertEquals(200, correctDecodes[2]);
for(int errorCount = 3; errorCount <= 8; errorCount++)
{
assertTrue(correctDecodes[errorCount] > 100, "Expected most decodes to be correct for " + errorCount +
" errors, but found " + correctDecodes[errorCount]);
}
}
/**
* Test: soft decision extraction of an error free codeword matches the hard decision extraction.
*/
@Test
void testExtractSoft()
{
Random random = new Random(2);
for(int trial = 0; trial < 20; trial++)
{
CorrectedBinaryMessage bptc = DMRBurstTestUtility.createInterleavedBPTC(random);
float[] softBits = new float[196];
for(int x = 0; x < 196; x++)
{
softBits[x] = bptc.get(x) ? 1.0f : -1.0f;
}
CorrectedBinaryMessage soft = BPTC_196_96.extractSoft(softBits);
assertNotNull(soft);
assertEquals(BPTC_196_96.extract(bptc).toString(), soft.toString());
assertEquals(0, soft.getCorrectedBitCount());
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.message;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* JUnit tests for the DMR Common Announcement Channel (CACH).
*/
public class CACHTest
{
/**
* Test: the CACH payload soft bits are de-interleaved consistently with the payload bits, so that each soft bit
* corresponds to the payload bit at the same position.
*/
@Test
void testPayloadSoftBits()
{
Random random = new Random(1);
for(int trial = 0; trial < 50; trial++)
{
CorrectedBinaryMessage burst = new CorrectedBinaryMessage(288);
float[] softBits = new float[288];
for(int x = 0; x < 288; x++)
{
if(random.nextBoolean())
{
burst.set(x);
}
//Unique magnitude per bit position so that a position mismatch can't go undetected
float magnitude = 1.0f + x / 1000.0f;
softBits[x] = burst.get(x) ? magnitude : -magnitude;
}
CACH cach = CACH.getCACH(burst, softBits);
BinaryMessage payload = cach.getPayload();
float[] payloadSoftBits = cach.getPayloadSoftBits();
assertNotNull(payloadSoftBits);
assertEquals(17, payloadSoftBits.length);
for(int x = 0; x < 17; x++)
{
assertEquals(payload.get(x), payloadSoftBits[x] > 0, "Trial " + trial + " payload bit " + x);
}
}
}
/**
* Test: payload soft bits are not available when the burst soft bits are not provided.
*/
@Test
void testNoSoftBits()
{
assertNull(CACH.getCACH(new CorrectedBinaryMessage(288)).getPayloadSoftBits());
assertNull(CACH.getCACH(new CorrectedBinaryMessage(288), null).getPayloadSoftBits());
}
}

View File

@@ -0,0 +1,328 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.message.data.lc.shorty;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Hamming17;
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the short link control assembler, which reassembles 4 CACH fragments into a BPTC(68,36) protected
* short link control message with a CRC-8 check.
*/
public class SLCAssemblerTest
{
private static final int ROW_LENGTH = 17;
private static final int ROW_INFORMATION_LENGTH = 12;
private static final List<Integer> ROW_CODEWORDS = new ArrayList<>();
static
{
//Hamming(17,12) codewords where bit i of the codeword is message bit i
for(int word = 0; word < (1 << ROW_LENGTH); word++)
{
BinaryMessage message = new BinaryMessage(ROW_LENGTH);
for(int i = 0; i < ROW_LENGTH; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
if(Hamming17.getSyndrome(message, 0) == 0)
{
ROW_CODEWORDS.add(word);
}
}
}
/**
* Finds the systematic row codeword whose information bits (the low 12 bits) match the information value.
*/
private static int encodeRow(int information)
{
for(int codeword: ROW_CODEWORDS)
{
if((codeword & ((1 << ROW_INFORMATION_LENGTH) - 1)) == information)
{
return codeword;
}
}
throw new IllegalStateException("No codeword for information value " + information);
}
/**
* Creates a 36-bit short link control payload of 28 random data bits followed by the CRC-8.
* @param random for the data bits
* @param validCRC true to append the correct CRC-8, or false to append a CRC-8 with one bit inverted.
*/
private static BinaryMessage createPayload(Random random, boolean validCRC)
{
BinaryMessage payload = new BinaryMessage(36);
for(int x = 0; x < 28; x++)
{
if(random.nextBoolean())
{
payload.set(x);
}
}
int crc = CRCDMR.crc8(payload, 28);
if(!validCRC)
{
crc ^= 1;
}
for(int x = 0; x < 8; x++)
{
if((crc & (1 << (7 - x))) != 0)
{
payload.set(28 + x);
}
}
return payload;
}
/**
* Encodes the 36-bit payload as a BPTC(68,36) message in transmitted (interleaved) bit order: 3 Hamming(17,12)
* rows carrying 12 payload bits each, followed by an even column parity row.
*/
private static BinaryMessage encode(BinaryMessage payload)
{
BinaryMessage deinterleaved = new BinaryMessage(68);
int parityRow = 0;
for(int row = 0; row < 4; row++)
{
int rowWord;
if(row < 3)
{
int information = 0;
for(int column = 0; column < ROW_INFORMATION_LENGTH; column++)
{
if(payload.get(row * ROW_INFORMATION_LENGTH + column))
{
information |= (1 << column);
}
}
rowWord = encodeRow(information);
parityRow ^= rowWord;
}
else
{
rowWord = parityRow;
}
for(int column = 0; column < ROW_LENGTH; column++)
{
if((rowWord & (1 << column)) != 0)
{
deinterleaved.set(row * ROW_LENGTH + column);
}
}
}
//De-interleave maps deinterleaved[(index * 17) % 67] = interleaved[index] (index 67 is not interleaved)
BinaryMessage interleaved = new BinaryMessage(68);
for(int index = 0; index < 67; index++)
{
if(deinterleaved.get((index * 17) % 67))
{
interleaved.set(index);
}
}
if(deinterleaved.get(67))
{
interleaved.set(67);
}
return interleaved;
}
/**
* Splits the interleaved message into 4 CACH fragments and processes them through the assembler.
* @return the short link control message dispatched with the last fragment.
*/
private static ShortLCMessage assemble(BinaryMessage interleaved)
{
return assemble(interleaved, null);
}
/**
* Splits the interleaved message and optional soft bits into 4 CACH fragments and processes them through the
* assembler.
* @param interleaved message
* @param softBits for the interleaved message, or null.
* @return the short link control message dispatched with the last fragment.
*/
private static ShortLCMessage assemble(BinaryMessage interleaved, float[] softBits)
{
SLCAssembler assembler = new SLCAssembler();
LCSS[] lcss = new LCSS[]{LCSS.FIRST_FRAGMENT, LCSS.CONTINUATION_FRAGMENT, LCSS.CONTINUATION_FRAGMENT,
LCSS.LAST_FRAGMENT};
ShortLCMessage message = null;
for(int fragment = 0; fragment < 4; fragment++)
{
BinaryMessage cachPayload = interleaved.getSubMessage(fragment * 17, fragment * 17 + 17);
float[] softFragment = softBits != null ? Arrays.copyOfRange(softBits, fragment * 17, fragment * 17 + 17) :
null;
message = assembler.process(lcss[fragment], cachPayload, softFragment, 0L);
if(fragment < 3)
{
assertNull(message, "Message dispatched before the last fragment");
}
}
assertNotNull(message, "Message not dispatched with the last fragment");
return message;
}
/**
* Test: an error free short link control message with a valid CRC is valid and carries the payload.
*/
@Test
void testValidMessage()
{
Random random = new Random(1);
for(int trial = 0; trial < 20; trial++)
{
BinaryMessage payload = createPayload(random, true);
ShortLCMessage message = assemble(encode(payload));
assertTrue(message.isValid(), "Trial " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(), "Trial " + trial);
}
}
/**
* Test: a short link control message with a single bit transmission error is corrected and valid.
*/
@Test
void testCorrectedMessage()
{
Random random = new Random(2);
for(int trial = 0; trial < 68; trial++)
{
BinaryMessage payload = createPayload(random, true);
BinaryMessage interleaved = encode(payload);
interleaved.flip(trial);
ShortLCMessage message = assemble(interleaved);
assertTrue(message.isValid(), "Bit error at " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(),
"Bit error at " + trial);
}
}
/**
* Test: a short link control message that passes BPTC error correction but fails the CRC-8 check is invalid.
* This occurs when the BPTC decoder converges on an incorrect codeword (miscorrection). The assembler previously
* overwrote the CRC-8 result with the BPTC result, reporting these messages as valid.
*/
@Test
void testInvalidCRC()
{
Random random = new Random(3);
for(int trial = 0; trial < 20; trial++)
{
ShortLCMessage message = assemble(encode(createPayload(random, false)));
assertEquals(0, message.getMessage().getCorrectedBitCount(), "Trial " + trial);
assertFalse(message.isValid(), "Trial " + trial);
}
}
/**
* Test: low reliability bit errors that defeat the hard decision decoder (decode failure, or a miscorrection that
* fails the CRC-8 check) are corrected by soft decision decoding when the fragment soft bits are provided.
*/
@Test
void testSoftDecision()
{
int hardFailures = 0;
for(int trial = 0; trial < 200 && hardFailures < 10; trial++)
{
Random random = new Random(100 + trial);
BinaryMessage payload = createPayload(random, true);
BinaryMessage interleaved = encode(payload);
float[] softBits = new float[68];
for(int x = 0; x < 68; x++)
{
softBits[x] = interleaved.get(x) ? 1.0f : -1.0f;
}
//Add 6 low reliability bit errors
Set<Integer> errors = new HashSet<>();
while(errors.size() < 6)
{
errors.add(random.nextInt(68));
}
for(int error: errors)
{
interleaved.flip(error);
softBits[error] = -0.2f * softBits[error];
}
//Only evaluate trials where the hard decision result is invalid
if(assemble(interleaved).isValid())
{
continue;
}
hardFailures++;
ShortLCMessage message = assemble(interleaved, softBits);
assertTrue(message.isValid(), "Trial " + trial);
assertEquals(payload.toString(), message.getMessage().getSubMessage(0, 36).toString(), "Trial " + trial);
}
assertEquals(10, hardFailures, "Expected to find 10 hard decision failures");
}
}