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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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