mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
#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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
288
src/main/java/io/github/dsheirer/edac/soft/SoftBlockCode.java
Normal file
288
src/main/java/io/github/dsheirer/edac/soft/SoftBlockCode.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user