mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
#1633 DMR Decoder redesign. Uses new DQPSK vector/scalar demodulator implementations and soft sync detectors for more accurate detections and symbol timing and symbol period calculations. Includes two new calibrations with scalar and vector implementations for DQPSK demodulator and Interpolator. Enhances DMR error detection and correction algorithms, specifically the block product turbo code algorithms. (#2115)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -164,6 +164,15 @@ application {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
if(os.isWindows()) {
|
||||
jvmArgs = jvmArgsWindows
|
||||
}
|
||||
else {
|
||||
jvmArgs = jvmArgsLinux
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes (
|
||||
|
||||
@@ -281,6 +281,20 @@ public class BinaryMessage extends BitSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bit1 and bit2 to this bitset.
|
||||
*
|
||||
* Note: this method does not provide overfill protection.
|
||||
*
|
||||
* @param bit1 to add.
|
||||
* @param bit2 to add.
|
||||
*/
|
||||
public void add(boolean bit1, boolean bit2)
|
||||
{
|
||||
set(mPointer++, bit1);
|
||||
set(mPointer++, bit2);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -56,15 +56,14 @@ import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility methods for designing various types of filters.
|
||||
*/
|
||||
@@ -627,11 +626,13 @@ public class FilterFactory
|
||||
* @param samplesPerSymbol - number of samples per symbol
|
||||
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
|
||||
* @param alpha - roll-off factor.
|
||||
* @param gain of the filter
|
||||
* @return - filter coefficients
|
||||
*/
|
||||
public static float[] getRootRaisedCosine(double samplesPerSymbol, int symbolCount, float alpha)
|
||||
{
|
||||
int taps = (int)(samplesPerSymbol * symbolCount);
|
||||
samplesPerSymbol /= 2;
|
||||
int taps = (int)Math.round(samplesPerSymbol * symbolCount);
|
||||
|
||||
float scale = 0;
|
||||
|
||||
@@ -639,47 +640,47 @@ public class FilterFactory
|
||||
|
||||
for(int x = 0; x < taps; x++)
|
||||
{
|
||||
float index = (float)x - ((float)taps / 2.0f);
|
||||
double index = x - (taps / 2.0f);
|
||||
|
||||
float x1 = (float)FastMath.PI * index / (float)samplesPerSymbol;
|
||||
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
|
||||
float x3 = x2 * x2 - 1.0f;
|
||||
double x1 = FastMath.PI * index / samplesPerSymbol;
|
||||
double x2 = 4.0 * alpha * index / samplesPerSymbol;
|
||||
double x3 = x2 * x2 - 1.0;
|
||||
|
||||
float numerator, denominator;
|
||||
double numerator, denominator;
|
||||
|
||||
if(FastMath.abs(x3) >= 0.000001)
|
||||
{
|
||||
if(x != taps / 2)
|
||||
{
|
||||
numerator = (float)FastMath.cos((1.0 + alpha) * x1) +
|
||||
(float)FastMath.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
|
||||
numerator = FastMath.cos((1.0 + alpha) * x1) +
|
||||
FastMath.sin((1.0 - alpha) * x1) / (4.0 * alpha * index / samplesPerSymbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
numerator = (float)FastMath.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)FastMath.PI / (4.0f * alpha);
|
||||
numerator = FastMath.cos((1.0 + alpha) * x1) + (1.0 - alpha) * FastMath.PI / (4.0 * alpha);
|
||||
}
|
||||
|
||||
denominator = x3 * (float)FastMath.PI;
|
||||
denominator = x3 * FastMath.PI;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(alpha == 1.0f)
|
||||
if(alpha == 1.0)
|
||||
{
|
||||
coefficients[x] = -1.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
x3 = (1.0f - alpha) * x1;
|
||||
x2 = (1.0f + alpha) * x1;
|
||||
x3 = (1.0 - alpha) * x1;
|
||||
x2 = (1.0 + alpha) * x1;
|
||||
|
||||
numerator = (float)(FastMath.sin(x2) * (1.0f + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
|
||||
(double)samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * (double)samplesPerSymbol *
|
||||
(double)samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
numerator = (FastMath.sin(x2) * (1.0 + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
|
||||
samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * samplesPerSymbol *
|
||||
samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
|
||||
denominator = (float)(-32.0 * FastMath.PI * alpha * alpha * index / (double)samplesPerSymbol);
|
||||
denominator = -32.0 * FastMath.PI * alpha * alpha * index / samplesPerSymbol;
|
||||
}
|
||||
|
||||
coefficients[x] = 4.0f * alpha * numerator / denominator;
|
||||
coefficients[x] = (float)(4.0 * alpha * numerator / denominator);
|
||||
|
||||
scale += coefficients[x];
|
||||
}
|
||||
@@ -1210,13 +1211,20 @@ public class FilterFactory
|
||||
{
|
||||
DecimalFormat df = new DecimalFormat("0.000000");
|
||||
|
||||
int length = 15;
|
||||
int length = 49;
|
||||
|
||||
float [] taps = FilterFactory.getHalfBand(length, WindowType.HAMMING);
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
try
|
||||
{
|
||||
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
|
||||
float [] taps = FilterFactory.getSinc(0.5, length, WindowType.NONE);
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mLog.debug("Done!");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
@@ -62,9 +61,10 @@ public class FilterViewer extends Application
|
||||
int filterLength = 47;
|
||||
WindowType windowType = WindowType.HAMMING;
|
||||
|
||||
float[] taps = FilterFactory.getHalfBand(filterLength, windowType);
|
||||
// float[] taps = FilterFactory.getHalfBand(filterLength, windowType);
|
||||
float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
|
||||
|
||||
taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
||||
// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* Copyright (C) 2002,2012 Free Software Foundation, Inc.
|
||||
* Ported to java from gnuradio - interpolator_taps.h
|
||||
*
|
||||
* 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/>
|
||||
******************************************************************************/
|
||||
public abstract class Interpolator
|
||||
{
|
||||
public static final int NTAPS = 8;
|
||||
@@ -158,4 +157,19 @@ public abstract class Interpolator
|
||||
{ -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
|
||||
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
|
||||
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
|
||||
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
|
||||
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
|
||||
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
|
||||
*
|
||||
* @param samples - sample array of length at least offset + 7
|
||||
* @param offset into the sample array for the start of the 8-value sequence
|
||||
* @param mu - interpolated sample position between 0.0 and 1.0
|
||||
* @return - interpolated sample value
|
||||
*/
|
||||
public abstract float filter(float[] samples, int offset, float mu);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
|
||||
/**
|
||||
* Factory for creating an optimal scalar or vector (SIMD) interpolator implementation
|
||||
*/
|
||||
public class InterpolatorFactory
|
||||
{
|
||||
/**
|
||||
* Selects and instantiates the best interpolator version, scalar or vector, based on previous calibration.
|
||||
* @return interpolator
|
||||
*/
|
||||
public static Interpolator getInterpolator()
|
||||
{
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.INTERPOLATOR);
|
||||
return getInterpolator(implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates the specified interpolator implementation.
|
||||
* @param implementation to construct.
|
||||
* @return interpolator.
|
||||
*/
|
||||
public static Interpolator getInterpolator(Implementation implementation)
|
||||
{
|
||||
return switch(implementation)
|
||||
{
|
||||
case VECTOR_SIMD_64 -> new InterpolatorVector64();
|
||||
case VECTOR_SIMD_128 -> new InterpolatorVector128();
|
||||
case VECTOR_SIMD_256, VECTOR_SIMD_512 -> new InterpolatorVector256(); //Vector 512 not supported
|
||||
case SCALAR, UNCALIBRATED -> new InterpolatorScalar();
|
||||
default -> throw new IllegalArgumentException("Unknown implementation type: " + implementation);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,35 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class RealInterpolator extends Interpolator
|
||||
public class InterpolatorScalar extends Interpolator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(InterpolatorScalar.class);
|
||||
|
||||
private float mGain;
|
||||
|
||||
@@ -21,11 +39,16 @@ public class RealInterpolator extends Interpolator
|
||||
*
|
||||
* @param gain to apply to the interpolated sample
|
||||
*/
|
||||
public RealInterpolator(float gain)
|
||||
public InterpolatorScalar(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public InterpolatorScalar()
|
||||
{
|
||||
this(1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
|
||||
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
|
||||
@@ -68,7 +91,7 @@ public class RealInterpolator extends Interpolator
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
RealInterpolator interpolator = new RealInterpolator(1.0f);
|
||||
InterpolatorScalar interpolator = new InterpolatorScalar(1.0f);
|
||||
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
|
||||
|
||||
double TWO_PI = FastMath.PI * 2.0;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 128 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector128 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
|
||||
FloatVector tapsVector, sampleVector;
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
return accumulator.reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 256 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector256 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
FloatVector sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
return sampleVector.mul(tapsVector).reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 64 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector64 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
|
||||
FloatVector tapsVector, sampleVector;
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 2);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 2);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 6);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 6);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
return accumulator.reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.filter.interpolator;
|
||||
|
||||
/**
|
||||
* Linear interpolator for values in the range: -PI > 0 > PI with awareness of phase wrapping across the
|
||||
* (+/-) PI boundary.
|
||||
*/
|
||||
public class PhaseAwareLinearInterpolator
|
||||
{
|
||||
public static final float TWO_PI = (float)(Math.PI * 2.0);
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value between x1 and x2 at a linear position mu between 0.0 and 1.0
|
||||
* @param x1 first value
|
||||
* @param x2 second value
|
||||
* @param mu offset between first and second values in range 0.0 to 1.0
|
||||
* @return interpolated value.
|
||||
*/
|
||||
public static float calculate(float x1, float x2, float mu)
|
||||
{
|
||||
//Detect phase wrap at the +PI/-PI boundary ... ignore opposite side of the unit circle at 0 axis
|
||||
if(x1 * x2 < -Math.PI)
|
||||
{
|
||||
if(x1 > 0)
|
||||
{
|
||||
float value = x1 + ((TWO_PI + x2 - x1) * mu);
|
||||
|
||||
if(value > Math.PI)
|
||||
{
|
||||
return -TWO_PI + value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float value = x1 + ((-TWO_PI + x2 - x1) * mu);
|
||||
|
||||
if(value < -Math.PI)
|
||||
{
|
||||
return TWO_PI + value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return x1 + ((x2 - x1) * mu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.dsp.filter.interpolator;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
/**
|
||||
* Base vector interpolator implementation.
|
||||
*/
|
||||
public abstract class VectorInterpolator extends Interpolator
|
||||
{
|
||||
@Override
|
||||
public float filter(float[] samples, int offset, float mu)
|
||||
{
|
||||
/* Ensure we have enough samples in the array */
|
||||
Validate.isTrue(samples.length >= offset + 7, "Offset [" + offset + "] must be 7 less than length[" + samples.length + "]");
|
||||
|
||||
//Identify the filter bank that corresponds to mu. Note: since we're not loading the TAPS in reverse order,
|
||||
//we select the inverse tap index that has the mirrored set of taps, by subtracting from 127.
|
||||
int index = 127 - (int)(NSTEPS * mu);
|
||||
return vectorFilter(samples, offset, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform vectorized interpolation to find the interpolated value from the samples where the 8-value sequence
|
||||
* starts at the offset into the array, using the filter coefficients specified by the index value.
|
||||
* @param samples to interpolate
|
||||
* @param offset to the start of the 8-value sequence
|
||||
* @param index into the TAPS coefficients array.
|
||||
* @return interpolated value.
|
||||
*/
|
||||
protected abstract float vectorFilter(float[] samples, int offset, int index);
|
||||
}
|
||||
@@ -1,23 +1,30 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* 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.
|
||||
* 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/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* 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.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.Interpolator;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorFactory;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
{
|
||||
@@ -29,6 +36,11 @@ public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
private Complex mPrecedingSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
|
||||
private final Interpolator mInterpolator = InterpolatorFactory.getInterpolator();
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("+#0.00000;-#0.00000");
|
||||
private float[] mI = new float[15];
|
||||
private float[] mQ = new float[15];
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier
|
||||
@@ -44,6 +56,32 @@ public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
super(interpolatingSampleBuffer, phaseLockedLoop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
super.receive(inphase, quadrature);
|
||||
|
||||
System.arraycopy(mI, 1, mI, 0, mI.length - 1);
|
||||
System.arraycopy(mQ, 1, mQ, 0, mQ.length - 1);
|
||||
|
||||
//Rotate the sample and add to buffer
|
||||
Complex current = new Complex(inphase, quadrature);
|
||||
current.normalizeFast();
|
||||
current.multiply(getPLL().getCurrentVector());
|
||||
mI[mI.length - 1] = current.inphase();
|
||||
mQ[mQ.length - 1] = current.quadrature();
|
||||
float iTemp = mInterpolator.filter(mI, 6, .416f);
|
||||
float qTemp = mInterpolator.filter(mQ, 6, .416f);
|
||||
float iSymbol = (mI[0] * iTemp) - (-mQ[0] * qTemp);
|
||||
float qSymbol = (mI[0] * qTemp) + (iTemp * -mQ[0]);
|
||||
float radians = (float)Math.atan2(iSymbol, qSymbol);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DECIMAL_FORMAT.format(radians)).append(",");
|
||||
sb.append(DECIMAL_FORMAT.format(((CostasLoop)getPLL()).getLoopFrequency()));
|
||||
// System.out.println(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a symbol from the interpolating buffer
|
||||
*/
|
||||
@@ -79,7 +117,7 @@ public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//Update the costas loop (PLL) with any measured phase error
|
||||
getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
// getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
|
||||
//Store current samples/symbols to use for the next symbol period
|
||||
mPreviousPrecedingSample.setValues(mPrecedingSample);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorScalar;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
@@ -45,7 +45,7 @@ public class InterpolatingSampleBuffer
|
||||
private float mMaximumSamplesPerSymbol;
|
||||
private float mMinimumSamplesPerSymbol;
|
||||
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
private InterpolatorScalar mInterpolator = new InterpolatorScalar(1.0f);
|
||||
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.Interpolator;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorFactory;
|
||||
import io.github.dsheirer.dsp.fm.IDemodulator;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator base class
|
||||
*/
|
||||
public abstract class DQPSKDemodulator implements IDemodulator
|
||||
{
|
||||
private int mSymbolRate;
|
||||
private float mSampleRate;
|
||||
private float mSamplesPerSymbol;
|
||||
protected float[] mIBuffer = new float[20]; //Initial size 20 for array copy, but gets resized on first buffer
|
||||
protected float[] mQBuffer = new float[20];
|
||||
protected float mMu;
|
||||
protected int mBufferOverlap;
|
||||
protected int mInterpolationOffset;
|
||||
protected final Interpolator mInterpolator = InterpolatorFactory.getInterpolator();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulator(double sampleRate, int symbolRate)
|
||||
{
|
||||
mSampleRate = (float) sampleRate;
|
||||
mSymbolRate = symbolRate;
|
||||
mSamplesPerSymbol = mSampleRate / mSymbolRate;
|
||||
mMu = mSamplesPerSymbol % 1; //Fractional part of the samples per symbol rate
|
||||
mInterpolationOffset = (int) Math.floor(mSamplesPerSymbol) - 4; //Interpolate at the middle of 8x samples
|
||||
mBufferOverlap = (int) Math.floor(mSamplesPerSymbol) + 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated samples per symbol.
|
||||
* @return samples per symbol.
|
||||
*/
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return mSamplesPerSymbol;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Factory for creating DQPSK demodulators
|
||||
*/
|
||||
public class DQPSKDemodulatorFactory
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(DQPSKDemodulatorFactory.class);
|
||||
|
||||
/**
|
||||
* Creates the optimal demodulator using calibration data to select the optimal implementation from scalar and
|
||||
* vector options, or defaults to the scalar implementation if the calibration wasn't executed.
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate in Hertz
|
||||
* @return demodulator instance
|
||||
*/
|
||||
public static DQPSKDemodulator getDemodulator(double sampleRate, int symbolRate)
|
||||
{
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.DQPSK_DEMODULATOR);
|
||||
|
||||
return switch(implementation)
|
||||
{
|
||||
case VECTOR_SIMD_64 -> new DQPSKDemodulatorVector64(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_128 -> new DQPSKDemodulatorVector128(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_256 -> new DQPSKDemodulatorVector256(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_512 -> new DQPSKDemodulatorVector512(sampleRate, symbolRate);
|
||||
default -> new DQPSKDemodulatorScalar(sampleRate, symbolRate); //Includes SCALAR case
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator that uses scalar calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DQPSKDemodulatorScalar extends DQPSKDemodulator
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorScalar.class);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulatorScalar(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] decodedPhases = new float[i.length];
|
||||
float iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x++)
|
||||
{
|
||||
iPrevious = mIBuffer[x];
|
||||
qPreviousConjugate = -mQBuffer[x]; //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
iCurrent = mInterpolator.filter(mIBuffer, offset, mu);
|
||||
qCurrent = mInterpolator.filter(mQBuffer, offset, mu);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = (iPrevious * iCurrent) - (qPreviousConjugate * qCurrent);
|
||||
differentialQ = (iPrevious * qCurrent) + (iCurrent * qPreviousConjugate);
|
||||
|
||||
decodedPhases[x] = (float)Math.atan2(differentialQ, differentialI);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator that uses Vector SIMD 128 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DQPSKDemodulatorVector128 extends DQPSKDemodulator
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector128.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulatorVector128(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[i.length];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator that uses Vector SIMD calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DQPSKDemodulatorVector256 extends DQPSKDemodulator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulatorVector256(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[sampleLength];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator that uses Vector SIMD 512 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DQPSKDemodulatorVector512 extends DQPSKDemodulator
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector512.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulatorVector512(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[sampleLength];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.dsp.psk.dqpsk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DQPSK demodulator that uses Vector SIMD 64 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DQPSKDemodulatorVector64 extends DQPSKDemodulator
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector64.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DQPSKDemodulatorVector64(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[i.length];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,61 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 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/>
|
||||
* * *****************************************************************************
|
||||
* 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.dsp.symbol;
|
||||
|
||||
public enum Dibit
|
||||
{
|
||||
D01_PLUS_3(false, true, 1, 4, 1),
|
||||
D00_PLUS_1(false, false, 0, 0, 0),
|
||||
D10_MINUS_1(true, false, 2, 8, 2),
|
||||
D11_MINUS_3(true, true, 3, 12, 3);
|
||||
D01_PLUS_3(false, true, 1, 4, 1, (float)(Math.PI / 4 * 3), (float)-Math.sin(Math.toRadians(45)), (float)Math.sin(Math.toRadians(45))),
|
||||
D00_PLUS_1(false, false, 0, 0, 0, (float)(Math.PI / 4.0), (float)Math.sin(Math.toRadians(45)), (float)Math.sin(Math.toRadians(45))),
|
||||
D10_MINUS_1(true, false, 2, 8, 2, (float)(Math.PI / -4.0), (float)Math.sin(Math.toRadians(45)), (float)-Math.sin(Math.toRadians(45))),
|
||||
D11_MINUS_3(true, true, 3, 12, 3, (float)(Math.PI / 4 * -3), (float)-Math.sin(Math.toRadians(45)), (float)-Math.sin(Math.toRadians(45)));
|
||||
|
||||
private boolean mBit1;
|
||||
private boolean mBit2;
|
||||
private int mLowValue;
|
||||
private int mHighValue;
|
||||
private int mValue;
|
||||
private float mPhase;
|
||||
private float mIdealI;
|
||||
private float mIdealQ;
|
||||
|
||||
private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value)
|
||||
/**
|
||||
* Dibit constructor.
|
||||
* @param bit1 boolean value
|
||||
* @param bit2 boolean value
|
||||
* @param lowValue integer value of the low order bit
|
||||
* @param highValue integer value of the high order bit
|
||||
* @param value integer value combined of both bits.
|
||||
* @param phase optimal phase value for this symbol when transmitted.
|
||||
* @param idealI ideal inphase value for this symbol when transmitted.
|
||||
* @param idealQ ideal quadrature value for this symbol when transmitted.
|
||||
*/
|
||||
Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value, float phase, float idealI, float idealQ)
|
||||
{
|
||||
mBit1 = bit1;
|
||||
mBit2 = bit2;
|
||||
mLowValue = lowValue;
|
||||
mHighValue = highValue;
|
||||
mValue = value;
|
||||
mPhase = phase;
|
||||
mIdealI = idealI;
|
||||
mIdealQ = idealQ;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +66,33 @@ public enum Dibit
|
||||
return mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideal phase angle when this symbol is transmitted.
|
||||
* @return ideal phase angle in radians.
|
||||
*/
|
||||
public float getIdealPhase()
|
||||
{
|
||||
return mPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideal inphase value when this symbol is transmitted.
|
||||
* @return ideal I
|
||||
*/
|
||||
public float getIdealI()
|
||||
{
|
||||
return mIdealI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideal quadrature value when this symbol is transmitted.
|
||||
* @return ideal Q
|
||||
*/
|
||||
public float getIdealQ()
|
||||
{
|
||||
return mIdealQ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowable transition state 1 for trellis coded modulation. Indicates
|
||||
* the dibit that differs from this dibit in the LSB bit position.
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/*
|
||||
Adopted from https://github.com/boatbod/op25/blob/e5e8fe457240fb358a8163bcf6fe7dda5e4d8392/op25/gr-op25_repeater/lib/hamming.cc
|
||||
*/
|
||||
public class BPTC_17_12_3 {
|
||||
|
||||
public static boolean decode17123(BinaryMessage d, int offset)
|
||||
{
|
||||
if(d==null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate the checksum this column should have
|
||||
boolean c0 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(6 + offset) ^ d.get(7 + offset) ^ d.get(9 + offset);
|
||||
boolean c1 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(7 + offset) ^ d.get(8 + offset) ^ d.get(10 + offset);
|
||||
boolean c2 = d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(8 + offset) ^ d.get(9 + offset) ^ d.get(11 + offset);
|
||||
boolean c3 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(7 + offset) ^ d.get(10 + offset);
|
||||
boolean c4 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(5 + offset) ^ d.get(6 + offset) ^ d.get(8 + offset) ^ d.get(11 + offset);
|
||||
|
||||
// Compare these with the actual bits
|
||||
byte n = 0x00;
|
||||
n |= (c0 != d.get(12 + offset)) ? 0x01 : 0x00;
|
||||
n |= (c1 != d.get(13 + offset)) ? 0x02 : 0x00;
|
||||
n |= (c2 != d.get(14 + offset)) ? 0x04 : 0x00;
|
||||
n |= (c3 != d.get(15 + offset)) ? 0x08 : 0x00;
|
||||
n |= (c4 != d.get(16 + offset)) ? 0x10 : 0x00;
|
||||
|
||||
switch (n) {
|
||||
// Parity bit errors
|
||||
case 0x01: d.set(12 + offset, !d.get(12 + offset)); return true;
|
||||
case 0x02: d.set(13 + offset, !d.get(13 + offset)); return true;
|
||||
case 0x04: d.set(14 + offset, !d.get(14 + offset)); return true;
|
||||
case 0x08: d.set(15 + offset, !d.get(15 + offset)); return true;
|
||||
case 0x10: d.set(16 + offset, !d.get(16 + offset)); return true;
|
||||
|
||||
// Data bit errors
|
||||
case 0x1B: d.set(0 + offset, !d.get(0 + offset)); return true;
|
||||
case 0x1F: d.set(1 + offset, !d.get(1 + offset)); return true;
|
||||
case 0x17: d.set(2 + offset, !d.get(2 + offset)); return true;
|
||||
case 0x07: d.set(3 + offset, !d.get(3 + offset)); return true;
|
||||
case 0x0E: d.set(4 + offset, !d.get(4 + offset)); return true;
|
||||
case 0x1C: d.set(5 + offset, !d.get(5 + offset)); return true;
|
||||
case 0x11: d.set(6 + offset, !d.get(6 + offset)); return true;
|
||||
case 0x0B: d.set(7 + offset, !d.get(7 + offset)); return true;
|
||||
case 0x16: d.set(8 + offset, !d.get(8 + offset)); return true;
|
||||
case 0x05: d.set(9 + offset, !d.get(9 + offset)); return true;
|
||||
case 0x0A: d.set(10 + offset,!d.get(10 + offset)); return true;
|
||||
case 0x14: d.set(11 + offset,!d.get(11 + offset)); return true;
|
||||
|
||||
// No bit errors
|
||||
case 0x00: return true;
|
||||
|
||||
// Unrecoverable errors
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.BitSetFullException;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) 196/96 Support
|
||||
*/
|
||||
public class BPTC_196_96
|
||||
{
|
||||
public static final int BPTC_LENGTH = 196;
|
||||
public static final int EXTRACTED_LENGTH = 96; //However, we set the 3x reserved bits in 96, 97, and 98 making the length 99
|
||||
public static final int MAX_ORIGINAL_INDEX = 136;
|
||||
public static final int COLUMN_COUNT = 15;
|
||||
public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
|
||||
public static final int CHECKSUM_COLUMN_COUNT = 4;
|
||||
public static final int MESSAGE_START_INDEX = 4;
|
||||
|
||||
/**
|
||||
* DMR De-interleaving indices
|
||||
*/
|
||||
public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182,
|
||||
167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3,
|
||||
184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20,
|
||||
5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37,
|
||||
22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54,
|
||||
39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71,
|
||||
56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103,
|
||||
88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120,
|
||||
105, 90, 75, 60, 45, 30, 15};
|
||||
|
||||
/**
|
||||
* DMR Column Indices
|
||||
*/
|
||||
public static int[][] COLUMN_INDEXES = new int[][]{
|
||||
{1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181},
|
||||
{2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182},
|
||||
{3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183},
|
||||
{4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184},
|
||||
{5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185},
|
||||
{6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186},
|
||||
{7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187},
|
||||
{8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188},
|
||||
{9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189},
|
||||
{10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190},
|
||||
{11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191},
|
||||
{12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192},
|
||||
{13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193},
|
||||
{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}};
|
||||
|
||||
/**
|
||||
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
|
||||
* protected message
|
||||
* @param original uncorrected BPTC(196,96) message
|
||||
* @return error corrected and extracted 96-bit message
|
||||
*/
|
||||
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
|
||||
{
|
||||
//De-Interleave
|
||||
CorrectedBinaryMessage message = deinterleave(original);
|
||||
|
||||
//Perform error detection and correction
|
||||
correct(message);
|
||||
|
||||
//Extract the 96-bit message from the corrected 196 bits
|
||||
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
|
||||
|
||||
//Transfer the corrected bit count
|
||||
extracted.setCorrectedBitCount(message.getCorrectedBitCount());
|
||||
|
||||
int index = MESSAGE_START_INDEX;
|
||||
|
||||
while(index < MAX_ORIGINAL_INDEX)
|
||||
{
|
||||
if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT)
|
||||
{
|
||||
try
|
||||
{
|
||||
extracted.add(message.get(index));
|
||||
}
|
||||
catch(BitSetFullException bsfe)
|
||||
{
|
||||
//Should never get to here
|
||||
bsfe.printStackTrace();
|
||||
return extracted;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
index += CHECKSUM_COLUMN_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
//Transfer bits R2, R1, and R0 to the end - RAS bits
|
||||
extracted.set(96, message.get(0));
|
||||
extracted.set(97, message.get(1));
|
||||
extracted.set(98, message.get(2));
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
|
||||
* @param message that contains interleaved (transmitted) message
|
||||
* @return de-interleaved message
|
||||
*/
|
||||
private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH);
|
||||
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
|
||||
{
|
||||
deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x]));
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the row level Hamming(15,11,3) protected words
|
||||
* @param message containing protected words in rows
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int index = -1;
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
//The first bit of the message is a pad/reserved bit and is ignored.
|
||||
offset = (row * 15) + 1;
|
||||
index = Hamming15.getErrorIndex(message, offset);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
rowErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Note: row errors should already be in order and don't need sorted
|
||||
return rowErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the column level Hamming(13,9,3) protected words
|
||||
* @param message containing protected words in columns
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> columnErrors = new ArrayList<>();
|
||||
|
||||
int index = -1;
|
||||
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
columnErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(columnErrors);
|
||||
return columnErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction on the DMR BPTC(196,96) protected message
|
||||
*
|
||||
* @param message to process and extract
|
||||
*/
|
||||
private static void correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = getRowErrors(message);
|
||||
|
||||
if(!rowErrors.isEmpty())
|
||||
{
|
||||
List<Integer> columnErrors = getColumnErrors(message);
|
||||
|
||||
if(matches(rowErrors, columnErrors))
|
||||
{
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
message.setCorrectedBitCount(rowErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Step 1: correct row errors and recalculate column errors (should be zero)
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
List<Integer> columnErrorsPart2 = getColumnErrors(message);
|
||||
|
||||
if(columnErrorsPart2.isEmpty())
|
||||
{
|
||||
//Success
|
||||
message.setCorrectedBitCount(rowErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Reverse the row error correction
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
//Step 2: correct column errors and recalculate row errors (should be zero)
|
||||
for(Integer index: columnErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
List<Integer> rowErrorsPart2 = getRowErrors(message);
|
||||
|
||||
if(rowErrorsPart2.isEmpty())
|
||||
{
|
||||
//Success
|
||||
message.setCorrectedBitCount(columnErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Reverse the column error correction
|
||||
for(Integer index: columnErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
//Set the error count greater than the maximum correctable column errors (16)
|
||||
message.setCorrectedBitCount(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the errors detected by the row protected words match the errors detected by the column protected words
|
||||
* @param rowErrors bit positions
|
||||
* @param columnErrors bit positions
|
||||
* @return true if they have the same quantity of bit position errors and the bit positions match
|
||||
*/
|
||||
private static boolean matches(List<Integer> rowErrors, List<Integer> columnErrors)
|
||||
{
|
||||
if(rowErrors.size() == columnErrors.size())
|
||||
{
|
||||
for(int x = 0; x < rowErrors.size(); x++)
|
||||
{
|
||||
if(!rowErrors.get(x).equals(columnErrors.get(0)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String raw = "0000000001100010100101100011100100110100110101000100000011101001110001001000100001001010010100001000011111011000000011110000111000111111100011000011100001100000000101001010000100010111010100111001";
|
||||
System.out.println("RAW:" + raw);
|
||||
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
System.out.println("LOD:" + message.toString());
|
||||
CorrectedBinaryMessage extracted = extract(message);
|
||||
System.out.println("EXT:" + extracted.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Implements Hamming(13,9,3) Error Detection algorithm
|
||||
@@ -8,8 +28,8 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
public class Hamming13
|
||||
{
|
||||
//DMR Checksums from generator matrix TS 102 361-1 Table B.14
|
||||
private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0XB, 0xC, 0x6, 0x3};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1};
|
||||
private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0xB, 0xC, 0x6, 0x3, 0x8, 0x4, 0x2, 0x1};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1, -1};
|
||||
|
||||
/**
|
||||
* Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message
|
||||
@@ -28,7 +48,7 @@ public class Hamming13
|
||||
return offset + ERROR_INDEX[syndrome];
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,11 +73,11 @@ public class Hamming13
|
||||
}
|
||||
else
|
||||
{
|
||||
return 10000 + errorIndex;
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +115,7 @@ public class Hamming13
|
||||
return calculated;
|
||||
}
|
||||
|
||||
private static int getSyndrome(BinaryMessage message, int[] indices)
|
||||
public static int getSyndrome(BinaryMessage message, int[] indices)
|
||||
{
|
||||
int calculated = calculateChecksum(message, indices);
|
||||
|
||||
@@ -126,4 +146,16 @@ public class Hamming13
|
||||
int checksum = message.getInt(offset + 9, offset + 12);
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String transmitted = "0000010000001";
|
||||
String correct = "0000000000000";
|
||||
String recommended = "0001000000001";
|
||||
// CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(transmitted));
|
||||
CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(correct));
|
||||
// CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(recommended));
|
||||
int syndrome = getSyndrome(cbm, 0);
|
||||
System.out.println("Syndrome " + syndrome);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
@@ -8,7 +27,7 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
public class Hamming15
|
||||
{
|
||||
//DMR Checksums from generator matrix TS 102 361-1 Table B.15
|
||||
private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0XA, 0x5, 0xB, 0xC, 0x6, 0x3};
|
||||
private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0xA, 0x5, 0xB, 0xC, 0x6, 0x3, 0x8, 0x4, 0x2, 0x1};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 14, 13, 10, 12, 6, 9, 4, 11, 0, 5, 7, 8, 1, 3, 2};
|
||||
|
||||
/**
|
||||
@@ -28,7 +47,7 @@ public class Hamming15
|
||||
return offset + ERROR_INDEX[syndrome];
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +77,7 @@ public class Hamming15
|
||||
* @param offset to bit 0 of the hamming protected word
|
||||
* @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage message, int offset)
|
||||
public static int getSyndrome(BinaryMessage message, int offset)
|
||||
{
|
||||
int calculated = calculateChecksum(message, offset);
|
||||
int checksum = message.getInt(offset + 11, offset + 14);
|
||||
|
||||
@@ -1,13 +1,63 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
|
||||
/**
|
||||
* Hamming(16,11,5) Error detection and correction utility.
|
||||
* Hamming(16,11,4) Error detection and correction utility. This code is capable of correcting single-bit errors and
|
||||
* detecting (but not correcting) double-bit errors.
|
||||
*/
|
||||
public class Hamming16
|
||||
public class Hamming16 implements IHamming
|
||||
{
|
||||
private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07};
|
||||
private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07, 0x10,
|
||||
0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
/**
|
||||
* Calculates the bit error index of the Hamming(16,11,4) protected word that is contained in the binary message
|
||||
* starting at the specified offset.
|
||||
*
|
||||
* @param message containing a Hamming protected word
|
||||
* @param offset to the start of the protected word
|
||||
* @return message index for an error bit or -1 if no errors are detected or 1000 if two bit errors are detected.
|
||||
*/
|
||||
public int getErrorIndex(BinaryMessage message, int offset)
|
||||
{
|
||||
int syndrome = getSyndrome(message, offset);
|
||||
|
||||
if(syndrome == 0)
|
||||
{
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
{
|
||||
return index + offset;
|
||||
}
|
||||
}
|
||||
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
|
||||
@@ -22,78 +72,36 @@ public class Hamming16
|
||||
{
|
||||
int syndrome = getSyndrome(frame, startIndex);
|
||||
|
||||
switch(syndrome)
|
||||
if (syndrome == 0)
|
||||
{
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
frame.flip(startIndex + 15); //Parity 1
|
||||
return 1;
|
||||
case 2:
|
||||
frame.flip(startIndex + 14); //Parity 2
|
||||
return 1;
|
||||
case 3:
|
||||
frame.flip(startIndex + 10); //Data 1
|
||||
return 1;
|
||||
case 4:
|
||||
frame.flip(startIndex + 13); //Parity 4
|
||||
return 1;
|
||||
case 5:
|
||||
frame.flip(startIndex + 9); //Data 2
|
||||
return 1;
|
||||
case 6:
|
||||
frame.flip(startIndex + 8); //Data 3
|
||||
return 1;
|
||||
case 7:
|
||||
frame.flip(startIndex + 7); //Data 4
|
||||
return 1;
|
||||
case 8:
|
||||
frame.flip(startIndex + 12); //Parity 8
|
||||
return 1;
|
||||
case 9:
|
||||
frame.flip(startIndex + 6); //Data 5
|
||||
return 1;
|
||||
case 10:
|
||||
frame.flip(startIndex + 5); //Data 6
|
||||
return 1;
|
||||
case 11:
|
||||
frame.flip(startIndex + 4); //Data 7
|
||||
return 1;
|
||||
case 12:
|
||||
frame.flip(startIndex + 3); //Data 8
|
||||
return 1;
|
||||
case 13:
|
||||
frame.flip(startIndex + 2); //Data 9
|
||||
return 1;
|
||||
case 14:
|
||||
frame.flip(startIndex + 1); //Data 10
|
||||
return 1;
|
||||
case 15:
|
||||
frame.flip(startIndex); //Data 11
|
||||
return 1;
|
||||
case 16:
|
||||
frame.flip(startIndex + 11); //Parity 16
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(CHECKSUMS[index] == syndrome)
|
||||
{
|
||||
frame.flip(startIndex + index);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum (Parity 16,8,4,2,1) for data (11 <> 1 ) bits.
|
||||
* Calculates the checksum from the transmitted data bits.
|
||||
*
|
||||
* @param frame - frame containing hamming protected word
|
||||
* @param startIndex - start bit index of the hamming protected word
|
||||
* @return parity value, 0 - 16
|
||||
* @return calculated checksum value
|
||||
*/
|
||||
private static int calculateChecksum(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = frame.nextSetBit(startIndex);
|
||||
i >= startIndex && i < startIndex + 11;
|
||||
i = frame.nextSetBit(i + 1))
|
||||
for(int i = frame.nextSetBit(startIndex); i >= startIndex && i < startIndex + 11; i = frame.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CHECKSUMS[i - startIndex];
|
||||
}
|
||||
@@ -109,12 +117,10 @@ public class Hamming16
|
||||
* @param startIndex - of bit 0 of the hamming protected word
|
||||
* @return - 0 (no errors) or 1 (single bit error corrected)
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
public static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = calculateChecksum(frame, startIndex);
|
||||
|
||||
int checksum = frame.getInt(startIndex + 11, startIndex + 15);
|
||||
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
@@ -5,81 +24,37 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
/**
|
||||
* Hamming(17,12,3) Error detection and correction utility.
|
||||
*/
|
||||
public class Hamming17
|
||||
public class Hamming17 implements IHamming
|
||||
{
|
||||
private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05};
|
||||
private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05,
|
||||
0x10, 0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
/**
|
||||
* Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
|
||||
* Calculates the bit error index of the Hamming(17,12,3) protected word that is contained in the binary message
|
||||
* starting at the specified offset.
|
||||
*
|
||||
* @param frame - binary frame containing a hamming(17,12,5) protected field
|
||||
* @param startIndex - offset to the first bit of the field
|
||||
* @return - 0 = no errors
|
||||
* 1 = a single-bit error was detected and corrected
|
||||
* 2 = two or more errors detected - no corrections made
|
||||
* @param message containing a Hamming protected word
|
||||
* @param offset to the start of the protected word
|
||||
* @return message index for an error bit or -1 if no errors are detected or 1000 if more than one bit error detected.
|
||||
*/
|
||||
public static int checkAndCorrect(BinaryMessage frame, int startIndex)
|
||||
public int getErrorIndex(BinaryMessage message, int offset)
|
||||
{
|
||||
int syndrome = getSyndrome(frame, startIndex);
|
||||
int syndrome = getSyndrome(message, offset);
|
||||
|
||||
switch(syndrome)
|
||||
if(syndrome == 0)
|
||||
{
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
frame.flip(startIndex + 16); //Parity 1
|
||||
return 1;
|
||||
case 2:
|
||||
frame.flip(startIndex + 15); //Parity 2
|
||||
return 1;
|
||||
case 3:
|
||||
frame.flip(startIndex + 11); //Data 1
|
||||
return 1;
|
||||
case 4:
|
||||
frame.flip(startIndex + 14); //Parity 4
|
||||
return 1;
|
||||
case 5:
|
||||
frame.flip(startIndex + 10); //Data 2
|
||||
return 1;
|
||||
case 6:
|
||||
frame.flip(startIndex + 9); //Data 3
|
||||
return 1;
|
||||
case 7:
|
||||
frame.flip(startIndex + 8); //Data 4
|
||||
return 1;
|
||||
case 8:
|
||||
frame.flip(startIndex + 13); //Parity 8
|
||||
return 1;
|
||||
case 9:
|
||||
frame.flip(startIndex + 7); //Data 5
|
||||
return 1;
|
||||
case 10:
|
||||
frame.flip(startIndex + 6); //Data 6
|
||||
return 1;
|
||||
case 11:
|
||||
frame.flip(startIndex + 5); //Data 7
|
||||
return 1;
|
||||
case 12:
|
||||
frame.flip(startIndex + 4); //Data 8
|
||||
return 1;
|
||||
case 13:
|
||||
frame.flip(startIndex + 3); //Data 9
|
||||
return 1;
|
||||
case 14:
|
||||
frame.flip(startIndex + 2); //Data 10
|
||||
return 1;
|
||||
case 15:
|
||||
frame.flip(startIndex + 1); //Data 11
|
||||
return 1;
|
||||
case 16:
|
||||
frame.flip(startIndex + 12); //Parity 16
|
||||
return 1;
|
||||
case 17:
|
||||
frame.flip(startIndex); //Data 12
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
{
|
||||
return index + offset;
|
||||
}
|
||||
}
|
||||
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,16 +62,14 @@ public class Hamming17
|
||||
*
|
||||
* @param frame - frame containing hamming protected word
|
||||
* @param startIndex - start bit index of the hamming protected word
|
||||
* @return parity value, 0 - 16
|
||||
* @return checksum value
|
||||
*/
|
||||
private static int calculateChecksum(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = frame.nextSetBit(startIndex);
|
||||
i >= startIndex && i < startIndex + 12;
|
||||
i = frame.nextSetBit(i + 1))
|
||||
for(int i = frame.nextSetBit(startIndex); i >= startIndex && i < startIndex + 12; i = frame.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CHECKSUMS[i - startIndex];
|
||||
}
|
||||
@@ -112,12 +85,10 @@ public class Hamming17
|
||||
* @param startIndex - of bit 0 of the hamming protected word
|
||||
* @return - 0 (no errors) or 1 (single bit error corrected)
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
public static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = calculateChecksum(frame, startIndex);
|
||||
|
||||
int checksum = frame.getInt(startIndex + 12, startIndex + 16);
|
||||
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/main/java/io/github/dsheirer/edac/IHamming.java
Normal file
39
src/main/java/io/github/dsheirer/edac/IHamming.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
|
||||
/**
|
||||
* Interface for Hamming error index calculator.
|
||||
*/
|
||||
public interface IHamming
|
||||
{
|
||||
int NO_ERRORS = -1;
|
||||
int MULTIPLE_ERRORS = 1000;
|
||||
|
||||
/**
|
||||
* Calculate the error index for the Hamming protected binary sequence
|
||||
* @param message containing the binary sequence
|
||||
* @param offset to the start of the binary sequence
|
||||
* @return -1 if no errors, single-bit error index, or 1000 if there is more than a single-bit error detected.
|
||||
*/
|
||||
int getErrorIndex(BinaryMessage message, int offset);
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 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.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.DoubleLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.PhaseLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.SymbolChart;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRDecoderInstrumented;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public class DMRPane extends ComplexDecoderPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRPane.class);
|
||||
|
||||
private HBox mSampleChartBox;
|
||||
private ComplexSampleLineChart mSampleLineChart;
|
||||
private EyeDiagramChart mEyeDiagramChart;
|
||||
private HBox mDecoderChartBox;
|
||||
private SymbolChart mSymbolChart;
|
||||
private PhaseLineChart mPLLPhaseErrorLineChart;
|
||||
private DoubleLineChart mPLLFrequencyLineChart;
|
||||
private DoubleLineChart mSamplesPerSymbolLineChart;
|
||||
private Broadcaster<ComplexSamples> mFilteredBufferBroadcaster = new Broadcaster();
|
||||
private DMRDecoderInstrumented mDecoder = new DMRDecoderInstrumented(new DecodeConfigDMR());
|
||||
|
||||
public DMRPane()
|
||||
{
|
||||
super(DecoderType.P25_PHASE1);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
addListener(nativeBuffer ->
|
||||
{
|
||||
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
|
||||
|
||||
while(iterator.hasNext())
|
||||
{
|
||||
getDecoder().receive(iterator.next());
|
||||
}
|
||||
});
|
||||
|
||||
getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
|
||||
getDecoder().setComplexSymbolListener(getSymbolChart());
|
||||
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
||||
getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
|
||||
getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
|
||||
getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
|
||||
mFilteredBufferBroadcaster.addListener(getSampleLineChart());
|
||||
|
||||
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
|
||||
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
|
||||
|
||||
mDecoder.setMessageListener(message -> mLog.debug(message.toString()));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
mLog.debug("Configuring for sample rate: " + sampleRate);
|
||||
|
||||
mDecoder.setSampleRate(sampleRate);
|
||||
double samplesPerSymbol = sampleRate / 4800.0;
|
||||
|
||||
getSampleLineChart().setSamplesPerSymbol((int) samplesPerSymbol);
|
||||
}
|
||||
|
||||
private DMRDecoderInstrumented getDecoder()
|
||||
{
|
||||
return mDecoder;
|
||||
}
|
||||
|
||||
private SymbolChart getSymbolChart()
|
||||
{
|
||||
if(mSymbolChart == null)
|
||||
{
|
||||
mSymbolChart = new SymbolChart(10);
|
||||
}
|
||||
|
||||
return mSymbolChart;
|
||||
}
|
||||
|
||||
private HBox getDecoderChartBox()
|
||||
{
|
||||
if(mDecoderChartBox == null)
|
||||
{
|
||||
mDecoderChartBox = new HBox();
|
||||
mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
|
||||
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
|
||||
getPLLFrequencyLineChart());
|
||||
}
|
||||
|
||||
return mDecoderChartBox;
|
||||
}
|
||||
|
||||
private HBox getSampleChartBox()
|
||||
{
|
||||
if(mSampleChartBox == null)
|
||||
{
|
||||
mSampleChartBox = new HBox();
|
||||
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
|
||||
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
|
||||
getSamplesPerSymbolLineChart());
|
||||
}
|
||||
|
||||
return mSampleChartBox;
|
||||
}
|
||||
|
||||
private ComplexSampleLineChart getSampleLineChart()
|
||||
{
|
||||
if(mSampleLineChart == null)
|
||||
{
|
||||
mSampleLineChart = new ComplexSampleLineChart("Raw Samples", 100, 10);
|
||||
}
|
||||
|
||||
return mSampleLineChart;
|
||||
}
|
||||
|
||||
private EyeDiagramChart getEyeDiagramChart()
|
||||
{
|
||||
if(mEyeDiagramChart == null)
|
||||
{
|
||||
mEyeDiagramChart = new EyeDiagramChart(10, "Symbol: 3-4");
|
||||
}
|
||||
|
||||
return mEyeDiagramChart;
|
||||
}
|
||||
|
||||
private PhaseLineChart getPLLPhaseErrorLineChart()
|
||||
{
|
||||
if(mPLLPhaseErrorLineChart == null)
|
||||
{
|
||||
mPLLPhaseErrorLineChart = new PhaseLineChart(40);
|
||||
}
|
||||
|
||||
return mPLLPhaseErrorLineChart;
|
||||
}
|
||||
|
||||
private DoubleLineChart getPLLFrequencyLineChart()
|
||||
{
|
||||
if(mPLLFrequencyLineChart == null)
|
||||
{
|
||||
mPLLFrequencyLineChart = new DoubleLineChart("PLL Frequency", -500, 500, 50, 40);
|
||||
}
|
||||
|
||||
return mPLLFrequencyLineChart;
|
||||
}
|
||||
|
||||
private DoubleLineChart getSamplesPerSymbolLineChart()
|
||||
{
|
||||
if(mSamplesPerSymbolLineChart == null)
|
||||
{
|
||||
mSamplesPerSymbolLineChart = new DoubleLineChart("Sample Point", 9.5, 11.5, 0.1, 40);
|
||||
}
|
||||
|
||||
return mSamplesPerSymbolLineChart;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,6 @@ package io.github.dsheirer.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class DecoderPaneFactory
|
||||
@@ -33,8 +32,7 @@ public class DecoderPaneFactory
|
||||
DecoderType.LTR_NET,
|
||||
DecoderType.MDC1200,
|
||||
DecoderType.MPT1327,
|
||||
DecoderType.TAIT_1200,
|
||||
DecoderType.DMR);
|
||||
DecoderType.TAIT_1200);
|
||||
|
||||
/**
|
||||
* Creates a decoder pane for the decoder type
|
||||
@@ -59,8 +57,6 @@ public class DecoderPaneFactory
|
||||
throw new IllegalArgumentException("Use the getP25P1DecoderPane() method for P25 decoder type");
|
||||
case P25_PHASE2:
|
||||
return new P25Phase2HDQPSKPane();
|
||||
case DMR:
|
||||
return new DMRPane();
|
||||
}
|
||||
|
||||
return getDefaultPane();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package io.github.dsheirer.gui.viewer;
|
||||
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRHardSymbolProcessor;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
@@ -168,7 +169,9 @@ public class DmrViewer extends VBox
|
||||
public void run()
|
||||
{
|
||||
List<IMessage> messages = new ArrayList<>();
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer();
|
||||
messageFramer.start();
|
||||
DMRHardSymbolProcessor symbolProcessor = new DMRHardSymbolProcessor(messageFramer);
|
||||
DecodeConfigDMR config = new DecodeConfigDMR();
|
||||
config.setUseCompressedTalkgroups(useCompressed);
|
||||
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
||||
@@ -180,7 +183,7 @@ public class DmrViewer extends VBox
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
messageFramer.receive(buffer);
|
||||
symbolProcessor.receive(buffer);
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.message;
|
||||
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Empty message to convey that a number of bits were processed for an empty timeslot.
|
||||
*/
|
||||
public class EmptyTimeslotPlaceholderMessage extends Message
|
||||
{
|
||||
private int mBitsProcessed;
|
||||
private Protocol mProtocol;
|
||||
private int mTimeslot = 0;
|
||||
|
||||
/**
|
||||
* Constructs a sync loss message.
|
||||
*
|
||||
* @param timestamp of the message
|
||||
* @param bitsProcessed without a sync pattern detection of message decode
|
||||
* @param timeslot that lost the sync
|
||||
*/
|
||||
public EmptyTimeslotPlaceholderMessage(long timestamp, int bitsProcessed, Protocol protocol, int timeslot)
|
||||
{
|
||||
super(timestamp);
|
||||
mBitsProcessed = bitsProcessed;
|
||||
mProtocol = protocol;
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a sync loss message.
|
||||
*
|
||||
* @param timestamp of the message
|
||||
* @param bitsProcessed without a sync pattern detection of message decode
|
||||
*/
|
||||
public EmptyTimeslotPlaceholderMessage(long timestamp, int bitsProcessed, Protocol protocol)
|
||||
{
|
||||
this(timestamp, bitsProcessed, protocol, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeslot that lost the sync
|
||||
* @return timeslot or a default of 0
|
||||
*/
|
||||
@Override
|
||||
public int getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the number of bits that were processed without a sync detection or message decode
|
||||
*/
|
||||
public int getBitsProcessed()
|
||||
{
|
||||
return mBitsProcessed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "EMPTY TIMESLOT - BITS PROCESSED [" + getBitsProcessed() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return mProtocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
/**
|
||||
* Timeslot message base class.
|
||||
*/
|
||||
public abstract class TimeslotMessage extends AbstractMessage
|
||||
public abstract class TimeslotMessage extends AbstractMessage implements IMessage
|
||||
{
|
||||
public static final int TIMESLOT_0 = 0;
|
||||
public static final int TIMESLOT_1 = 1;
|
||||
|
||||
@@ -479,7 +479,7 @@ public class DecoderFactory
|
||||
AliasList aliasList, DecodeConfigDMR decodeConfig,
|
||||
TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor)
|
||||
{
|
||||
modules.add(new DMRDecoder(decodeConfig));
|
||||
modules.add(new DMRDecoder(decodeConfig, channel.isTrafficChannel()));
|
||||
|
||||
DMRTrafficChannelManager dmrTrafficChannelManager = null;
|
||||
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitDelayBuffer;
|
||||
import io.github.dsheirer.dsp.symbol.QPSKCarrierLock;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Processes a stream of Dibit symbols and performs burst detection and timeslot framing. This framer
|
||||
* also detects abnormal PLL phase locks and issues PLL phase lock corrections.
|
||||
*
|
||||
* Timeslot Map:
|
||||
* 0: Common Association Channel (CACH) and Slow Link Control (SLC)
|
||||
* 1. Timeslot 1
|
||||
* 2. Timeslot 2
|
||||
*/
|
||||
public class DMRBurstFramer implements Listener<Dibit>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRBurstFramer.class);
|
||||
|
||||
private static final int DMR_SYMBOL_RATE = 4800;
|
||||
private static final double PLL_PHASE_CORRECTION_90_DEGREES = (double)DMR_SYMBOL_RATE / 4.0;
|
||||
private static final double PLL_PHASE_CORRECTION_180_DEGREES = (double)DMR_SYMBOL_RATE / 2.0;
|
||||
|
||||
private static final int BURST_DIBIT_START_TS1 = 0;
|
||||
private static final int BURST_DIBIT_START_TS2 = 144;
|
||||
private static final int BURST_DIBIT_LENGTH = 144;
|
||||
private static final int TWO_TIMESLOT_DIBIT_LENGTH = BURST_DIBIT_LENGTH * 2;
|
||||
private static final int SYNC_DIBIT_OFFSET_TS1 = 66;
|
||||
private static final int SYNC_DIBIT_OFFSET_TS2 = 210;
|
||||
private static final int SYNC_DIBIT_LENGTH = 24;
|
||||
private static final int MAX_STREAMING_SYNC_DETECT_BIT_ERRORS = 4;
|
||||
private static final int MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS = 6;
|
||||
|
||||
/**
|
||||
* Threshold for issuing a sync loss message. This is set to trigger once the dibit count exceeds one second of
|
||||
* dibits (4800 baud/dibits) plus two bursts dibit length. The two burst length padding is so that we don't reset
|
||||
* the dibit counter below the burst sync check threshold.
|
||||
*/
|
||||
private static final int SYNC_LOSS_MESSAGE_THRESHOLD = DMR_SYMBOL_RATE + TWO_TIMESLOT_DIBIT_LENGTH;
|
||||
|
||||
/**
|
||||
* The message buffer is sized to hold two DMR bursts of 144 dibits (288 bits) each.
|
||||
*/
|
||||
private DibitDelayBuffer mMessageBuffer = new DibitDelayBuffer(BURST_DIBIT_LENGTH * 2);
|
||||
|
||||
/**
|
||||
* The sync delay buffer is sized to align sync detection with two bursts being fully loaded into the message and
|
||||
* timeslot 1 region of the message buffer is aligned with the sync delay buffer.
|
||||
*
|
||||
* This delay equals the number of dibits in the second half of the burst payload: 54 dibits (108 bits).
|
||||
*/
|
||||
private DibitDelayBuffer mSyncDelayBuffer = new DibitDelayBuffer(54);
|
||||
|
||||
/**
|
||||
* Synchronized indicates that either the primary or secondary sync tracker is currently synchronized to
|
||||
* one of the timeslot bursts. Once either sync tracker obtains a valid sync pattern, this flag is set
|
||||
* to true. The synchronized state will remain true as long as at least one of the two sync trackers
|
||||
* maintains sync state. Once both sync trackers lose sync, the framer will fallback into sync search/detection
|
||||
* mode to inspect each dibit until a sync match can be (re)discovered.
|
||||
*/
|
||||
private boolean mSynchronized = false;
|
||||
|
||||
/**
|
||||
* Tracks the number of dibits received to trigger burst or sync loss message processing
|
||||
*/
|
||||
private int mDibitCounter = 0;
|
||||
|
||||
/**
|
||||
* The Sync detection is used to gain initial lock to a sync pattern or to regain sync if lost. The sync detector
|
||||
* is used to align a DMR burst into the timeslot 1 region of the message dibit delay buffer so that it can be
|
||||
* inspected to ensure the timeslots are aligned.
|
||||
*/
|
||||
private DMRSyncDetector mSyncDetectorTimeslot1 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS,
|
||||
MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS);
|
||||
private DMRSyncDetector mSyncDetectorTimeslot2 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS,
|
||||
MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS);
|
||||
|
||||
/**
|
||||
* Timeslot sync trackers track the synchronization state for each of the two timeslots.
|
||||
*/
|
||||
private SyncTracker mSyncTrackerTimeslot1 = new SyncTracker();
|
||||
private SyncTracker mSyncTrackerTimeslot2 = new SyncTracker();
|
||||
|
||||
/**
|
||||
* Timeslot alignment tracker tracks that the timeslots in the delay buffer are aligned.
|
||||
*/
|
||||
private TimeslotAlignmentTracker mTimeslotAlignmentTracker = new TimeslotAlignmentTracker();
|
||||
|
||||
private IDMRBurstDetectListener mBurstDetectListener;
|
||||
private IPhaseLockedLoop mPhaseLockedLoop;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param listener to be notified of framed burst detections and/or sync loss bits processed
|
||||
* @param phaseLockedLoop to receive symbol alignment corrections
|
||||
*/
|
||||
public DMRBurstFramer(IDMRBurstDetectListener listener, IPhaseLockedLoop phaseLockedLoop)
|
||||
{
|
||||
mBurstDetectListener = listener;
|
||||
mPhaseLockedLoop = phaseLockedLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary dibit symbol input method
|
||||
* @param dibit to process
|
||||
*/
|
||||
@Override
|
||||
public void receive(Dibit dibit)
|
||||
{
|
||||
mDibitCounter++;
|
||||
|
||||
//Feed the message buffer first to ensure buffer contains two full bursts when a sync is detected
|
||||
mMessageBuffer.put(dibit);
|
||||
|
||||
//Feed the sync delay buffer and reassign the delayed dibit to feed the sync detector if we're not synchronized
|
||||
dibit = mSyncDelayBuffer.getAndPut(dibit);
|
||||
|
||||
if(mSynchronized)
|
||||
{
|
||||
if(mDibitCounter >= TWO_TIMESLOT_DIBIT_LENGTH)
|
||||
{
|
||||
if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH)
|
||||
{
|
||||
processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0);
|
||||
}
|
||||
|
||||
dispatch();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mSyncDetectorTimeslot2.add(dibit);
|
||||
|
||||
if(mSyncDetectorTimeslot2.hasSync())
|
||||
{
|
||||
dispatch();
|
||||
}
|
||||
else if(mDibitCounter >= SYNC_LOSS_MESSAGE_THRESHOLD)
|
||||
{
|
||||
processSyncLossDibits(DMR_SYMBOL_RATE, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches burst 1 and/or burst 2 that is currently in the message buffer
|
||||
*/
|
||||
private void dispatch()
|
||||
{
|
||||
if(mBurstDetectListener != null)
|
||||
{
|
||||
if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH)
|
||||
{
|
||||
processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0);
|
||||
}
|
||||
|
||||
mSyncDetectorTimeslot1.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS1));
|
||||
mSyncDetectorTimeslot2.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS2));
|
||||
|
||||
mSyncTrackerTimeslot1.update(mSyncDetectorTimeslot1.getSyncPattern(), mSyncDetectorTimeslot1.getCarrierLock());
|
||||
mSyncTrackerTimeslot2.update(mSyncDetectorTimeslot2.getSyncPattern(), mSyncDetectorTimeslot2.getCarrierLock());
|
||||
|
||||
CorrectedBinaryMessage burst1 = null;
|
||||
CorrectedBinaryMessage burst2 = null;
|
||||
|
||||
CACH cach1 = null;
|
||||
CACH cach2 = null;
|
||||
|
||||
//If we have valid sync but PLL tracker misalignment, correct both timeslots before we extract messages
|
||||
if((mSyncTrackerTimeslot1.hasSync() && mSyncTrackerTimeslot1.hasCarrierMisAlignment()) ||
|
||||
(mSyncTrackerTimeslot2.hasSync() && mSyncTrackerTimeslot2.hasCarrierMisAlignment()))
|
||||
{
|
||||
// mLog.debug("Repairing Message Bits - PLL Mis-Alignment Detected");
|
||||
if(mSyncTrackerTimeslot1.hasCarrierMisAlignment())
|
||||
{
|
||||
repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot1.getCarrierLock());
|
||||
repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot1.getCarrierLock());
|
||||
}
|
||||
else
|
||||
{
|
||||
repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot2.getCarrierLock());
|
||||
repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot2.getCarrierLock());
|
||||
}
|
||||
}
|
||||
|
||||
if(mSyncTrackerTimeslot1.hasSync())
|
||||
{
|
||||
burst1 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS1, BURST_DIBIT_LENGTH);
|
||||
burst1.incrementCorrectedBitCount(mSyncDetectorTimeslot1.getPatternMatchBitErrorCount());
|
||||
|
||||
if(mSyncTrackerTimeslot1.getSyncPattern().hasCACH())
|
||||
{
|
||||
cach1 = CACH.getCACH(burst1);
|
||||
}
|
||||
}
|
||||
|
||||
if(mSyncTrackerTimeslot2.hasSync())
|
||||
{
|
||||
burst2 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS2, BURST_DIBIT_LENGTH);
|
||||
burst2.incrementCorrectedBitCount(mSyncDetectorTimeslot2.getPatternMatchBitErrorCount());
|
||||
|
||||
if(mSyncTrackerTimeslot2.getSyncPattern().hasCACH())
|
||||
{
|
||||
cach2 = CACH.getCACH(burst2);
|
||||
}
|
||||
}
|
||||
|
||||
//Update the timeslot alignment tracker using the sync patterns and optional CACHes from each timeslot
|
||||
mTimeslotAlignmentTracker.update(cach1, cach2, mSyncDetectorTimeslot1.getSyncPattern(),
|
||||
mSyncDetectorTimeslot2.getSyncPattern());
|
||||
|
||||
if(mTimeslotAlignmentTracker.hasSufficientData())
|
||||
{
|
||||
if(mTimeslotAlignmentTracker.isAligned())
|
||||
{
|
||||
if(burst1 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 1);
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
processSyncLossDibits(BURST_DIBIT_LENGTH, 1);
|
||||
}
|
||||
|
||||
if(burst2 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 2);
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
processSyncLossDibits(BURST_DIBIT_LENGTH, 2);
|
||||
}
|
||||
|
||||
mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(burst1 != null)
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 2);
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
|
||||
//Transfer the sync pattern from tracker 1 to tracker 2 so we don't lose voice framing
|
||||
mSyncTrackerTimeslot2.setSyncPattern(mSyncTrackerTimeslot1.getSyncPattern());
|
||||
}
|
||||
else
|
||||
{
|
||||
processSyncLossDibits(BURST_DIBIT_LENGTH, 2);
|
||||
mSyncTrackerTimeslot2.reset();
|
||||
}
|
||||
|
||||
mTimeslotAlignmentTracker.reset();
|
||||
mSyncTrackerTimeslot1.reset();
|
||||
mDibitCounter = BURST_DIBIT_LENGTH;
|
||||
mSynchronized = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
|
||||
//repeater or we have some form of mobile burst. If mobile, process to the correct timeslto, otherwise
|
||||
//dump the messages to timeslot 0 so that the burst doesn't corrupt the decoder states for either
|
||||
// timeslot, until we positively regain timeslot tracking
|
||||
if(burst1 != null)
|
||||
{
|
||||
DMRSyncPattern sync1 = mSyncTrackerTimeslot1.getSyncPattern();
|
||||
|
||||
if(sync1.isMobileStationSyncPattern() || sync1.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 1);
|
||||
}
|
||||
else if(sync1.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst1, sync1, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
processSyncLossDibits(BURST_DIBIT_LENGTH, 0);
|
||||
}
|
||||
|
||||
if(burst2 != null)
|
||||
{
|
||||
DMRSyncPattern sync2 = mSyncTrackerTimeslot2.getSyncPattern();
|
||||
|
||||
if(sync2.isMobileStationSyncPattern() || sync2.isDirectModeTS1())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 1);
|
||||
}
|
||||
else if(sync2.isDirectModeTS2())
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurstDetectListener.burstDetected(burst2, sync2, 0);
|
||||
}
|
||||
|
||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
processSyncLossDibits(BURST_DIBIT_LENGTH, 0);
|
||||
}
|
||||
|
||||
mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync();
|
||||
}
|
||||
|
||||
if(!mSynchronized)
|
||||
{
|
||||
mTimeslotAlignmentTracker.reset();
|
||||
mSyncTrackerTimeslot1.reset();
|
||||
mSyncTrackerTimeslot2.reset();
|
||||
}
|
||||
|
||||
if(mDibitCounter < 0)
|
||||
{
|
||||
mDibitCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repairs the captured dibits in the message buffer when a PLL lock misalignment is detected.
|
||||
* @param offset to start of burst
|
||||
* @param carrierLock that indicates the mis-alignment
|
||||
*/
|
||||
private void repairPLLMisalignment(int offset, QPSKCarrierLock carrierLock)
|
||||
{
|
||||
int end = offset + BURST_DIBIT_LENGTH;
|
||||
|
||||
for(int x = offset; x < end; x++)
|
||||
{
|
||||
Dibit misalignedDibit = mMessageBuffer.get(x);
|
||||
mMessageBuffer.set(x, carrierLock.correct(misalignedDibit));
|
||||
}
|
||||
|
||||
if(mPhaseLockedLoop != null)
|
||||
{
|
||||
// mLog.warn("Repairing PLL Phase Lock Mis-Alignment - Detected: " + carrierLock);
|
||||
switch(carrierLock)
|
||||
{
|
||||
case PLUS_90:
|
||||
mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_90_DEGREES);
|
||||
break;
|
||||
case MINUS_90:
|
||||
mPhaseLockedLoop.correctInversion(-PLL_PHASE_CORRECTION_90_DEGREES);
|
||||
break;
|
||||
case INVERTED:
|
||||
mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_180_DEGREES);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the message and sync delay buffers and resets the dibit counter to zero.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mDibitCounter = 0;
|
||||
mMessageBuffer.reset();
|
||||
mSyncDelayBuffer.reset();
|
||||
mTimeslotAlignmentTracker.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of the sync field currently in the message buffer as specified by the offset value.
|
||||
*/
|
||||
private long getSyncValue(int dibitOffset)
|
||||
{
|
||||
Dibit[] syncDibits = mMessageBuffer.getBuffer(dibitOffset, SYNC_DIBIT_LENGTH);
|
||||
|
||||
long value = 0;
|
||||
|
||||
for(Dibit dibit: syncDibits)
|
||||
{
|
||||
value = Long.rotateLeft(value, 2);
|
||||
value += dibit.getValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes dibits received without a sync condition
|
||||
* @param dibitCount with sync loss
|
||||
*/
|
||||
private void processSyncLossDibits(int dibitCount, int timeslot)
|
||||
{
|
||||
mDibitCounter -= dibitCount;
|
||||
|
||||
if(mBurstDetectListener != null)
|
||||
{
|
||||
mBurstDetectListener.syncLost(dibitCount * 2, timeslot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the sync state for a timeslot to account for voice framing that does not transmit a sync pattern in each
|
||||
* frame.
|
||||
*/
|
||||
public class SyncTracker
|
||||
{
|
||||
private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
|
||||
private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL;
|
||||
|
||||
/**
|
||||
* Updates the sync pattern for the most recently detected burst to the specified pattern. When the previous
|
||||
* sync pattern is one of either base or mobile voice frame and the detected pattern is unknown, automatically
|
||||
* advances the sync pattern to the next voice frame in the super frame sequence to account for voice
|
||||
* frames B - F that do not have a sync pattern.
|
||||
*/
|
||||
public void update(DMRSyncPattern pattern, QPSKCarrierLock carrierLock)
|
||||
{
|
||||
if(pattern == DMRSyncPattern.UNKNOWN)
|
||||
{
|
||||
switch(mSyncPattern)
|
||||
{
|
||||
case BASE_STATION_VOICE: //Voice frame A
|
||||
mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_B;
|
||||
break;
|
||||
case BS_VOICE_FRAME_B:
|
||||
mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_C;
|
||||
break;
|
||||
case BS_VOICE_FRAME_C:
|
||||
mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_D;
|
||||
break;
|
||||
case BS_VOICE_FRAME_D:
|
||||
mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_E;
|
||||
break;
|
||||
case BS_VOICE_FRAME_E:
|
||||
mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_F;
|
||||
break;
|
||||
|
||||
case MOBILE_STATION_VOICE: //Voice frame A
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_1:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_2:
|
||||
mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_B;
|
||||
break;
|
||||
case MS_VOICE_FRAME_B:
|
||||
mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_C;
|
||||
break;
|
||||
case MS_VOICE_FRAME_C:
|
||||
mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_D;
|
||||
break;
|
||||
case MS_VOICE_FRAME_D:
|
||||
mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_E;
|
||||
break;
|
||||
case MS_VOICE_FRAME_E:
|
||||
mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_F;
|
||||
break;
|
||||
default:
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = carrierLock;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = carrierLock;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Most recently detected sync pattern
|
||||
*/
|
||||
public DMRSyncPattern getSyncPattern()
|
||||
{
|
||||
return mSyncPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly sets the pattern for this tracker
|
||||
*/
|
||||
public void setSyncPattern(DMRSyncPattern pattern)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrier lock as indicated by the sync detector.
|
||||
*/
|
||||
public QPSKCarrierLock getCarrierLock()
|
||||
{
|
||||
return mCarrierLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the burst has a QPSK constellation mis-alignment that has to be corrected
|
||||
*/
|
||||
public boolean hasCarrierMisAlignment()
|
||||
{
|
||||
return getCarrierLock() != QPSKCarrierLock.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the contents of the message buffer to determine if the message has a valid sync pattern, or if the
|
||||
* message is a continuation voice frame from a voice super frame. The identified sync pattern and number of
|
||||
* bit errors for that pattern are stored in the sync detector so that they can be accessed for dispatching
|
||||
* a message.
|
||||
*/
|
||||
public boolean hasSync()
|
||||
{
|
||||
return getSyncPattern() != DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the sync pattern to unknown
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mSyncPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,13 @@ package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -49,9 +44,9 @@ import org.slf4j.LoggerFactory;
|
||||
* values against CSBK messages and some other function is involved in determining which CRC mask value to use when
|
||||
* checking the CRC of the transmitted CSBK ... fun and games!
|
||||
*/
|
||||
public class DmrCrcMaskManager
|
||||
public class DMRCrcMaskManager
|
||||
{
|
||||
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
|
||||
private Logger LOGGER = LoggerFactory.getLogger(DMRCrcMaskManager.class);
|
||||
private Map<Integer, MaskTracker> mCsbkTrackerMapTS1 = new TreeMap<>();
|
||||
private Map<Integer, MaskTracker> mCsbkTrackerMapTS2 = new TreeMap<>();
|
||||
private Set<MaskTracker> mSortedTrackersTS1 = new TreeSet<>();
|
||||
@@ -60,7 +55,7 @@ public class DmrCrcMaskManager
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DmrCrcMaskManager()
|
||||
public DMRCrcMaskManager()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,22 +19,26 @@
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.gain.complex.ComplexGainFactory;
|
||||
import io.github.dsheirer.dsp.gain.complex.IComplexGainControl;
|
||||
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
|
||||
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
|
||||
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorFactory;
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.SyncLossMessage;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl;
|
||||
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.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -44,7 +48,9 @@ import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||
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;
|
||||
@@ -52,108 +58,82 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DMR decoder module.
|
||||
* DMR DQPSK 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* ensure accurate burst detection and tracking.
|
||||
*
|
||||
* The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and
|
||||
* embedded link control parameters.
|
||||
*/
|
||||
public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IComplexSamplesListener, Listener<ComplexSamples>, IByteBufferProvider
|
||||
public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
|
||||
ISourceEventProvider, Listener<ComplexSamples>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRDecoder.class);
|
||||
protected static final float SAMPLE_COUNTER_GAIN = 0.4f;
|
||||
private static final double SYMBOL_RATE = 4800.0;
|
||||
private double mSampleRate;
|
||||
private Broadcaster<Dibit> mDibitBroadcaster = new Broadcaster<>();
|
||||
private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300);
|
||||
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 Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
|
||||
private DQPSKDemodulator mDemodulator;
|
||||
private DMRMessageFramer mMessageFramer = new DMRMessageFramer();
|
||||
private DMRSoftSymbolProcessor mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer);
|
||||
private DMRMessageProcessor mMessageProcessor;
|
||||
protected IComplexGainControl mAGC = ComplexGainFactory.getComplexGainControl();
|
||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||
protected IRealFilter mIBasebandFilter;
|
||||
protected IRealFilter mQBasebandFilter;
|
||||
|
||||
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
|
||||
protected CostasLoop mCostasLoop;
|
||||
protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
|
||||
protected DMRMessageFramer mMessageFramer;
|
||||
protected PowerMonitor mPowerMonitor = new PowerMonitor();
|
||||
private IRealFilter mIBasebandFilter;
|
||||
private IRealFilter mQBasebandFilter;
|
||||
private PowerMonitor mPowerMonitor = new PowerMonitor();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param config for the DMR decoder
|
||||
*/
|
||||
public DMRDecoder(DecodeConfigDMR config)
|
||||
public DMRDecoder(DecodeConfigDMR config, boolean isTrafficChannel)
|
||||
{
|
||||
mMessageProcessor = new DMRMessageProcessor(config);
|
||||
mMessageProcessor.setMessageListener(getMessageListener());
|
||||
getDibitBroadcaster().addListener(mByteBufferAssembler);
|
||||
setSampleRate(25000.0);
|
||||
|
||||
if(isTrafficChannel)
|
||||
{
|
||||
mSymbolProcessor.setBaseStationMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
super.start();
|
||||
mQPSKDemodulator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
super.stop();
|
||||
mQPSKDemodulator.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* DMR decoder type.
|
||||
*/
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.DMR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message processor
|
||||
*/
|
||||
protected DMRMessageProcessor getMessageProcessor()
|
||||
{
|
||||
return mMessageProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate and configures internal decoder components.
|
||||
* @param sampleRate of the channel to decode
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
if(sampleRate <= getSymbolRate() * 2)
|
||||
if(sampleRate <= SYMBOL_RATE * 2)
|
||||
{
|
||||
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
|
||||
getSymbolRate() + " symbol rate)");
|
||||
SYMBOL_RATE + " symbol rate)");
|
||||
}
|
||||
|
||||
mPowerMonitor.setSampleRate((int)sampleRate);
|
||||
mSampleRate = sampleRate;
|
||||
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter());
|
||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||
mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300);
|
||||
mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this);
|
||||
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||
|
||||
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
|
||||
|
||||
if(mMessageFramer != null)
|
||||
{
|
||||
getDibitBroadcaster().removeListener(mMessageFramer);
|
||||
}
|
||||
|
||||
//The Costas Loop receives symbol-inversion correction requests when detected.
|
||||
//The PLL gain monitor receives sync detect/loss signals from the message framer
|
||||
mMessageFramer = new DMRMessageFramer(mCostasLoop);
|
||||
mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
|
||||
mMessageFramer.setListener(getMessageProcessor());
|
||||
|
||||
mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
|
||||
getDibitBroadcaster().addListener(mMessageFramer);
|
||||
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
||||
mDemodulator = DQPSKDemodulatorFactory.getDemodulator(sampleRate, SYMBOL_RATE);
|
||||
mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol());
|
||||
mMessageFramer.setListener(mMessageProcessor);
|
||||
mMessageProcessor.setMessageListener(getMessageListener());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,142 +143,86 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
mMessageFramer.setTimestamp(samples.timestamp());
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
|
||||
//Process buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
float[] demodulated = mDemodulator.demodulate(i, q);
|
||||
mSymbolProcessor.receive(demodulated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a baseband filter for this decoder using the current sample rate
|
||||
*/
|
||||
private float[] getBasebandFilter()
|
||||
private float[] getBasebandFilter(double sampleRate)
|
||||
{
|
||||
//Attempt to reuse a cached (ie already-designed) filter if available
|
||||
float[] filter = mBasebandFilters.get(getSampleRate());
|
||||
|
||||
if(filter == null)
|
||||
if(BASEBAND_FILTERS.containsKey(sampleRate))
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate((int)getSampleRate())
|
||||
return BASEBAND_FILTERS.get(sampleRate);
|
||||
}
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification
|
||||
.lowPassBuilder()
|
||||
.sampleRate(sampleRate)
|
||||
.passBandCutoff(5100)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandStart(6500)
|
||||
.stopBandRipple(0.01)
|
||||
.build();
|
||||
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
|
||||
.stopBandAmplitude(0.0).stopBandStart(6500) //6500
|
||||
.stopBandRipple(0.01).build();
|
||||
|
||||
try
|
||||
{
|
||||
filter = FilterFactory.getTaps(specification);//
|
||||
}
|
||||
catch(Exception fde) //FilterDesignException
|
||||
{
|
||||
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
|
||||
}
|
||||
float[] coefficients = null;
|
||||
|
||||
if(filter != null)
|
||||
{
|
||||
mBasebandFilters.put(getSampleRate(), filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalStateException("Couldn't design a DMR baseband filter for sample rate: " + getSampleRate());
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process source events
|
||||
*/
|
||||
protected void process(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
try
|
||||
{
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
mCostasLoop.reset();
|
||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
|
||||
mCostasLoop.reset();
|
||||
break;
|
||||
coefficients = FilterFactory.getTaps(specification);
|
||||
BASEBAND_FILTERS.put(sampleRate, coefficients);
|
||||
}
|
||||
catch(Exception fde) //FilterDesignException
|
||||
{
|
||||
System.out.println("Error");
|
||||
}
|
||||
|
||||
if(coefficients == null)
|
||||
{
|
||||
throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]");
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this decoder to prepare for processing a new channel
|
||||
*/
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mCostasLoop.reset();
|
||||
mFrequencyCorrectionSyncMonitor.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembler for packaging Dibit stream into reusable byte buffers.
|
||||
*/
|
||||
protected Broadcaster<Dibit> getDibitBroadcaster()
|
||||
{
|
||||
return mDibitBroadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
|
||||
* Implements the IByteBufferProvider interface - delegates to the symbol processor
|
||||
*/
|
||||
@Override
|
||||
public void setBufferListener(Listener<ByteBuffer> listener)
|
||||
{
|
||||
mByteBufferAssembler.setBufferListener(listener);
|
||||
mSymbolProcessor.setBufferListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
|
||||
* Implements the IByteBufferProvider interface - delegates to the symbol processor
|
||||
*/
|
||||
@Override
|
||||
public void removeBufferListener(Listener<ByteBuffer> listener)
|
||||
{
|
||||
mByteBufferAssembler.removeBufferListener(listener);
|
||||
mSymbolProcessor.setBufferListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
|
||||
* Implements the IByteBufferProvider interface - delegates to the symbol processor
|
||||
*/
|
||||
@Override
|
||||
public boolean hasBufferListeners()
|
||||
{
|
||||
return mByteBufferAssembler.hasBufferListeners();
|
||||
return mSymbolProcessor.hasBufferListener();
|
||||
}
|
||||
|
||||
protected double getSymbolRate()
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return SYMBOL_RATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current sample rate for this decoder
|
||||
*/
|
||||
protected double getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples per symbol based on current sample rate and symbol rate.
|
||||
*/
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return (float)(getSampleRate() / getSymbolRate());
|
||||
return this::process;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,66 +231,194 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
super.setSourceEventListener(listener);
|
||||
mPowerMonitor.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a registered source event listener from receiving source events from this decoder
|
||||
*/
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
super.removeSourceEventListener();
|
||||
mPowerMonitor.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
public void start()
|
||||
{
|
||||
return sourceEvent -> process(sourceEvent);
|
||||
super.start();
|
||||
mMessageFramer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
super.stop();
|
||||
mMessageFramer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener interface to receive reusable complex buffers
|
||||
* Process source events
|
||||
*/
|
||||
private void process(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ComplexSamples> getComplexSamplesListener()
|
||||
{
|
||||
return DMRDecoder.this;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
File file = new File("/home/denny/Downloads/TIII,HYT414.585000,.wav");
|
||||
LOGGER.info("Starting ...");
|
||||
|
||||
try
|
||||
// String directory = "D:\\DQPSK Equalizer Research\\"; //Windows
|
||||
String directory = "/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";
|
||||
// String file = directory + "20230819_064344_454575000_JPJ_Communications_(DMR)_Madison_Control_28_baseband.wav";
|
||||
// String file = directory + "DMR_DCDM_4_4_baseband_20220318_142716.wav";
|
||||
// String file = directory + "DMR_4_20241213_Saianet.wav";
|
||||
String file = directory + "DMR_5_20241217_031219_451425000_SaiaNet_Onondaga_SaiaNet_Control_1_baseband.wav";
|
||||
// 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_10_CAP_ENCRYPTED_20241222_035408_935487500_American_Airlines_Maricopa_Control_1_baseband.wav";
|
||||
|
||||
boolean autoReplay = false;
|
||||
|
||||
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR(), false);
|
||||
decoder.start();
|
||||
|
||||
UserPreferences userPreferences = new UserPreferences();
|
||||
DMRAudioModule audio1 = new DMRAudioModule(userPreferences, new AliasList(""), DMRMessage.TIMESLOT_1);
|
||||
DMRAudioModule audio2 = new DMRAudioModule(userPreferences, new AliasList(""), DMRMessage.TIMESLOT_2);
|
||||
|
||||
decoder.setMessageListener(new Listener<>()
|
||||
{
|
||||
private long mBitCounter = 1;
|
||||
private int mBitErrorCounter;
|
||||
private int mValidMessageCounter;
|
||||
private int mTotalMessageCounter;
|
||||
|
||||
@Override
|
||||
public void receive(IMessage iMessage)
|
||||
{
|
||||
int errors = 0;
|
||||
|
||||
if(iMessage.getTimeslot() == DMRMessage.TIMESLOT_1)
|
||||
{
|
||||
audio1.receive(iMessage);
|
||||
}
|
||||
if(iMessage.getTimeslot() == DMRMessage.TIMESLOT_2)
|
||||
{
|
||||
audio2.receive(iMessage);
|
||||
}
|
||||
|
||||
if(iMessage instanceof DMRBurst burst)
|
||||
{
|
||||
mBitCounter += 288;
|
||||
errors = burst.getMessage().getCorrectedBitCount();
|
||||
mTotalMessageCounter++;
|
||||
|
||||
if(mTotalMessageCounter == 492)
|
||||
{
|
||||
int a = 0;
|
||||
}
|
||||
if(burst.isValid())
|
||||
{
|
||||
mBitErrorCounter += Math.max(errors, 0);
|
||||
mValidMessageCounter++;
|
||||
}
|
||||
}
|
||||
else if(iMessage instanceof LCMessage lcw)
|
||||
{
|
||||
mTotalMessageCounter++;
|
||||
errors = lcw.getMessage().getCorrectedBitCount();
|
||||
if(lcw.isValid())
|
||||
{
|
||||
mBitErrorCounter += errors;
|
||||
mValidMessageCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
double bitErrorRate = (double)mBitErrorCounter / (double)mBitCounter * 100.0;
|
||||
|
||||
boolean logFLC = false;
|
||||
boolean logSLC = false;
|
||||
boolean logCACH = false;
|
||||
boolean logIdles = false;
|
||||
boolean logEverything = true;
|
||||
|
||||
if(!logEverything && logFLC)
|
||||
{
|
||||
if(iMessage instanceof FullLCMessage)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
else if(iMessage instanceof Terminator terminator)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
else if(iMessage instanceof DataMessageWithLinkControl)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
}
|
||||
|
||||
if(!logEverything && logCACH && iMessage instanceof DMRBurst burst && burst.hasCACH())
|
||||
{
|
||||
System.out.println("CACH:" + burst.getCACH());
|
||||
}
|
||||
|
||||
if(!logEverything && logSLC && iMessage instanceof ShortLCMessage)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
|
||||
if(logEverything)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
|
||||
if(!logEverything && logIdles && iMessage instanceof SyncLossMessage)
|
||||
{
|
||||
System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay))
|
||||
{
|
||||
ComplexWaveSource source = new ComplexWaveSource(file);
|
||||
source.open();
|
||||
System.out.println("Source Sample Rate: " + source.getSampleRate());
|
||||
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR());
|
||||
decoder.setMessageListener(message -> System.out.println("TS:" + message.getTimeslot() + " " + message));
|
||||
source.setListener(iNativeBuffer -> {
|
||||
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
ComplexSamples samples = it.next();
|
||||
decoder.receive(samples);
|
||||
decoder.receive(it.next());
|
||||
}
|
||||
});
|
||||
decoder.setSampleRate(source.getSampleRate());
|
||||
decoder.start();
|
||||
source.start();
|
||||
decoder.setSampleRate(source.getSampleRate());
|
||||
|
||||
while(true)
|
||||
{
|
||||
source.next(65535);
|
||||
source.next(2048, true);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(IOException ioe)
|
||||
{
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Error", ioe);
|
||||
}
|
||||
|
||||
LOGGER.info("Finished");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 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.psk.DQPSKDecisionDirectedDemodulatorInstrumented;
|
||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
|
||||
/**
|
||||
* Instrumented version of DMR decoder
|
||||
*/
|
||||
public class DMRDecoderInstrumented extends DMRDecoder
|
||||
{
|
||||
private Listener<Double> mPLLPhaseErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<ComplexSamples> mFilteredSymbolListener;
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
|
||||
/**
|
||||
* Instrumented version of the DMR decoder that supports registering listeners to provide access to data as
|
||||
* it is being processed by the decoder.
|
||||
*/
|
||||
public DMRDecoderInstrumented(DecodeConfigDMR config)
|
||||
{
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for processing incoming complex sample buffers
|
||||
* @param complexSamples containing channelized complex samples
|
||||
*/
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
|
||||
if(mFilteredSymbolListener != null)
|
||||
{
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
|
||||
}
|
||||
|
||||
//Process buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides this method so we can correctly configure for instrumented operations
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
InterpolatingSampleBufferInstrumented instrumentedBuffer =
|
||||
new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||
mInterpolatingSampleBuffer = instrumentedBuffer;
|
||||
|
||||
DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
|
||||
mQPSKDemodulator = instrumented;
|
||||
|
||||
instrumented.setComplexSymbolListener(mComplexSymbolListener);
|
||||
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
|
||||
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
|
||||
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
|
||||
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
|
||||
instrumented.setSymbolListener(getDibitBroadcaster());
|
||||
getDibitBroadcaster().addListener(mMessageFramer);
|
||||
}
|
||||
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLPhaseErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLPhaseErrorListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
|
||||
}
|
||||
|
||||
public void setFilteredBufferListener(Listener<ComplexSamples> listener)
|
||||
{
|
||||
mFilteredSymbolListener = listener;
|
||||
}
|
||||
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
|
||||
}
|
||||
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedEncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
@@ -546,7 +547,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
if(message.getSyncPattern().isMobileSyncPattern())
|
||||
{
|
||||
if(message.getSyncPattern().isDirectMode())
|
||||
if(message.getSyncPattern().isDirect())
|
||||
{
|
||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||
}
|
||||
@@ -569,7 +570,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
|
||||
|
||||
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
|
||||
if(embedded.getShortBurst() instanceof EmbeddedEncryptionParameters arc4)
|
||||
{
|
||||
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
|
||||
}
|
||||
@@ -1356,10 +1357,10 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
/**
|
||||
* Updates the current call with encryption information.
|
||||
* @param encryptionParameters decoded from the Voice Frame F
|
||||
* @param embeddedEncryptionParameters decoded from the Voice Frame F
|
||||
* @param isGroup true for group or false for individual call.
|
||||
*/
|
||||
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
|
||||
private void updateEncryptedCall(EmbeddedEncryptionParameters embeddedEncryptionParameters, boolean isGroup, long timestamp)
|
||||
{
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
@@ -1367,11 +1368,11 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
|
||||
if(details == null)
|
||||
{
|
||||
details = encryptionParameters.toString();
|
||||
details = embeddedEncryptionParameters.toString();
|
||||
}
|
||||
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
|
||||
else if(!details.contains(embeddedEncryptionParameters.toString()) && !details.contains("ENCRYPTION"))
|
||||
{
|
||||
details += " " + encryptionParameters;
|
||||
details += " " + embeddedEncryptionParameters;
|
||||
}
|
||||
|
||||
mCurrentCallEvent.setDetails(details);
|
||||
@@ -1381,7 +1382,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
mCurrentCallEvent = DMRDecodeEvent.builder(isGroup ? DecodeEventType.CALL_GROUP_ENCRYPTED :
|
||||
DecodeEventType.CALL_ENCRYPTED, timestamp)
|
||||
.channel(getCurrentChannel())
|
||||
.details(encryptionParameters.toString())
|
||||
.details(embeddedEncryptionParameters.toString())
|
||||
.identifiers(getIdentifierCollection().copyOf())
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.symbol.Dibit;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRHardSyncDetector;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Processes a stream of dibit symbol decisions to detect DMR sync patterns and produce decoded DMR messages.
|
||||
*
|
||||
* Note: this symbol processor is primarily used for offline analysis of a captured bit stream recording.
|
||||
*/
|
||||
public class DMRHardSymbolProcessor
|
||||
{
|
||||
private final DMRMessageFramer mMessageFramer;
|
||||
private final DMRHardSyncDetector mSyncDetector = new DMRHardSyncDetector();
|
||||
//Dibit delay line sizing: CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
|
||||
private DibitDelayLine mDibitDelayLine = new DibitDelayLine(90);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param messageFramer to receive dibits and sync notifications.
|
||||
*/
|
||||
public DMRHardSymbolProcessor(DMRMessageFramer messageFramer)
|
||||
{
|
||||
mMessageFramer = messageFramer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an individual dibit.
|
||||
* @param dibit to process
|
||||
*/
|
||||
public void process(Dibit dibit)
|
||||
{
|
||||
Dibit ejected = mDibitDelayLine.insert(dibit);
|
||||
mMessageFramer.receive(ejected);
|
||||
|
||||
if(mSyncDetector.process(dibit))
|
||||
{
|
||||
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for streaming decoded symbol byte arrays.
|
||||
*
|
||||
* @param buffer to process into a stream of dibits for processing.
|
||||
*/
|
||||
public void receive(ByteBuffer buffer)
|
||||
{
|
||||
for(byte value : buffer.array())
|
||||
{
|
||||
for(int x = 0; x <= 3; x++)
|
||||
{
|
||||
process(Dibit.parse(value, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -16,517 +16,375 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.SyncLossMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.audio.DMRCallSequenceRecorder;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessageFactory;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.Identity;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.token.Token;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* DMR Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors
|
||||
* and issue phase corrections.
|
||||
* DMR message framer. Processes a DMR symbol stream to produce DMR messages/bursts. Employs two symbol buffers to
|
||||
* track and frame both DMR timeslots. Responds to external sync detections to alternately activate one of the two
|
||||
* buffers and capture a DMR burst, convert it to the correct message format and send it to a message listener.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public class DMRMessageFramer implements Listener<Dibit>, IDMRBurstDetectListener
|
||||
public class DMRMessageFramer implements Listener<Dibit>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageFramer.class);
|
||||
|
||||
private static final double DMR_BIT_RATE = 9600.0;
|
||||
|
||||
/**
|
||||
* Provides sync detection and burst framing
|
||||
*/
|
||||
private DMRBurstFramer mBurstFramer;
|
||||
|
||||
/**
|
||||
* External listener for messages produced by this framer
|
||||
*/
|
||||
private static final int DIBIT_CACH_START = 0;
|
||||
private static final int DIBIT_BURST_END = 144;
|
||||
private Listener<IMessage> mMessageListener;
|
||||
|
||||
/**
|
||||
* External listener for sync state information
|
||||
*/
|
||||
private ISyncDetectListener mSyncDetectListener;
|
||||
|
||||
/**
|
||||
* Tracks current time for use with framed bursts/messages. This value is updated externally from sample buffer
|
||||
* timestamps and internally as each framed message is received or sync loss is processed.
|
||||
*/
|
||||
private long mCurrentTime = System.currentTimeMillis();
|
||||
private Dibit[] mBufferA = new Dibit[144];
|
||||
private Dibit[] mBufferB = new Dibit[144];
|
||||
private int mBufferAPointer = 0;
|
||||
private int mBufferBPointer = 0;
|
||||
private DMRSyncPattern mBufferAPattern = DMRSyncPattern.UNKNOWN;
|
||||
private DMRSyncPattern mBufferBPattern = DMRSyncPattern.UNKNOWN;
|
||||
private int mBufferATimeslot;
|
||||
private int mBufferBTimeslot;
|
||||
private boolean mBufferAActive = false;
|
||||
private boolean mAssemblingBurst = false;
|
||||
private int mDibitCounter = 0;
|
||||
private int mDibitSinceTimestampCounter = 0;
|
||||
private long mReferenceTimestamp = 0;
|
||||
private boolean mRunning = false;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param phaseLockedLoop to receive PLL phase lock error corrections that are identified by the DMRBurstFramer
|
||||
* when known phase misaligned sync patterns are detected.
|
||||
*/
|
||||
public DMRMessageFramer(IPhaseLockedLoop phaseLockedLoop)
|
||||
{
|
||||
mBurstFramer = new DMRBurstFramer(this, phaseLockedLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*/
|
||||
public DMRMessageFramer()
|
||||
{
|
||||
this(null);
|
||||
Arrays.fill(mBufferA, Dibit.D00_PLUS_1);
|
||||
Arrays.fill(mBufferB, Dibit.D00_PLUS_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a sync detect listener to be notified each time a sync pattern and NID are detected.
|
||||
*/
|
||||
public void setSyncDetectListener(ISyncDetectListener syncDetectListener)
|
||||
{
|
||||
mSyncDetectListener = syncDetectListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current timestamp or timestamp of incoming message buffers that is continuously updated to as
|
||||
* close as possible to the bits processed for the expected baud rate.
|
||||
*
|
||||
* @return current time
|
||||
*/
|
||||
private long getTimestamp()
|
||||
{
|
||||
return mCurrentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current time. This should be invoked by an incoming message buffer stream.
|
||||
*
|
||||
* @param currentTime
|
||||
*/
|
||||
public void setCurrentTime(long currentTime)
|
||||
{
|
||||
mCurrentTime = currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current timestamp based on the number of bits processed versus the bit rate per second
|
||||
* in order to keep an accurate running timestamp to use for timestamped message creation.
|
||||
*
|
||||
* @param bitsProcessed thus far
|
||||
*/
|
||||
private void updateBitsProcessed(int bitsProcessed)
|
||||
{
|
||||
if(bitsProcessed > 0)
|
||||
{
|
||||
mCurrentTime += (long)((double)bitsProcessed / DMR_BIT_RATE * 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener for messages produced by this message framer
|
||||
*
|
||||
* @param messageListener to receive framed and decoded messages
|
||||
*/
|
||||
public void setListener(Listener<IMessage> messageListener)
|
||||
{
|
||||
mMessageListener = messageListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for streaming decoded symbol dibits for message framing.
|
||||
*
|
||||
* @param dibit to process
|
||||
* Primary input method for receiving the stream of demodulated dibit symbols.
|
||||
* @param dibit to process.
|
||||
*/
|
||||
@Override
|
||||
public void receive(Dibit dibit)
|
||||
{
|
||||
mBurstFramer.receive(dibit);
|
||||
}
|
||||
mDibitCounter++;
|
||||
mDibitSinceTimestampCounter++;
|
||||
|
||||
/**
|
||||
* Primary method for streaming decoded symbol byte arrays.
|
||||
*
|
||||
* @param buffer to process into a stream of dibits for processing.
|
||||
*/
|
||||
public void receive(ByteBuffer buffer)
|
||||
{
|
||||
for(byte value : buffer.array())
|
||||
//Issue a sync loss for each full 1-second interval (ie 4800 dibits) of no sync.
|
||||
if(!mAssemblingBurst && mDibitCounter >= 4800)
|
||||
{
|
||||
for(int x = 0; x <= 3; x++)
|
||||
dispatch(new SyncLossMessage(getTimestamp(), 9600, Protocol.DMR, DMRMessage.TIMESLOT_0));
|
||||
mDibitCounter -= 4800;
|
||||
mBufferAPattern = DMRSyncPattern.UNKNOWN;
|
||||
mBufferBPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
|
||||
if(mAssemblingBurst)
|
||||
{
|
||||
if(mBufferAActive)
|
||||
{
|
||||
receive(Dibit.parse(value, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
mBufferA[mBufferAPointer++] = dibit;
|
||||
|
||||
/**
|
||||
* DMR burst detection processing. Processes DMR burst/messages from external burst framer/producer.
|
||||
*
|
||||
* @param message bits for the burst/message and any bit errors detected in the sync pattern
|
||||
* @param syncPattern that was detected for the burst
|
||||
*/
|
||||
@Override
|
||||
public void burstDetected(CorrectedBinaryMessage message, DMRSyncPattern syncPattern, int timeslot)
|
||||
{
|
||||
//Each burst is 288 bits
|
||||
updateBitsProcessed(288);
|
||||
|
||||
//This allows the frequency error monitor to be notified when we're in sync
|
||||
if(mSyncDetectListener != null)
|
||||
{
|
||||
mSyncDetectListener.syncDetected(0);
|
||||
}
|
||||
|
||||
CACH cach = null;
|
||||
|
||||
if(syncPattern.hasCACH())
|
||||
{
|
||||
cach = CACH.getCACH(message);
|
||||
}
|
||||
|
||||
if(mMessageListener != null)
|
||||
{
|
||||
DMRMessage dmrMessage = DMRMessageFactory.create(syncPattern, message, cach, getTimestamp(), timeslot);
|
||||
|
||||
if(dmrMessage != null)
|
||||
{
|
||||
mMessageListener.receive(dmrMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a sync loss to track how many bits were processed and update message listeners.
|
||||
*
|
||||
* @param bitsProcessed since the last sync detect
|
||||
*/
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed, int timeslot)
|
||||
{
|
||||
updateBitsProcessed(bitsProcessed);
|
||||
|
||||
dispatchSyncLoss(bitsProcessed, timeslot);
|
||||
|
||||
if(mSyncDetectListener != null)
|
||||
{
|
||||
mSyncDetectListener.syncLost(bitsProcessed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sync loss message and dispatches it to the message listener
|
||||
*
|
||||
* @param bitsProcessed without a sync detection
|
||||
* @param timeslot that lost the sync
|
||||
*/
|
||||
private void dispatchSyncLoss(int bitsProcessed, int timeslot)
|
||||
{
|
||||
if(bitsProcessed > 0 && mMessageListener != null)
|
||||
{
|
||||
mMessageListener.receive(new SyncLossMessage(getTimestamp(), bitsProcessed, Protocol.DMR, timeslot));
|
||||
}
|
||||
}
|
||||
|
||||
public static class MessageListener implements Listener<IMessage>
|
||||
{
|
||||
private boolean mHasDMRData = false;
|
||||
public int mTS0Count = 0;
|
||||
public int mTS1Count = 0;
|
||||
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message instanceof DMRBurst && ((DMRBurst)message).hasCACH())
|
||||
{
|
||||
CACH cach = ((DMRBurst)message).getCACH();
|
||||
mLog.info(cach.toString() + " TS:" + message.getTimeslot() + " " + message.toString());
|
||||
if(mBufferAPointer >= mBufferA.length)
|
||||
{
|
||||
dispatchBufferA();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info(" TS:" + message.getTimeslot() + " " + message.toString());
|
||||
}
|
||||
mBufferB[mBufferBPointer++] = dibit;
|
||||
|
||||
if(message instanceof DMRPacketMessage dpm)
|
||||
{
|
||||
if(dpm.getPacket() instanceof LRRPPacket lrrp)
|
||||
if(mBufferBPointer >= mBufferB.length)
|
||||
{
|
||||
mLog.info("LRRP: " + lrrp.toString());
|
||||
mLog.info("MESSAGE: " + lrrp.getMessage().toHexString());
|
||||
dispatchBufferB();
|
||||
}
|
||||
}
|
||||
|
||||
// if(message instanceof UDTHeader)
|
||||
// {
|
||||
// mLog.info("TS:" + message.getTimeslot() + " " + message.toString());
|
||||
// }
|
||||
|
||||
// if(message instanceof UnknownCSBKMessage && message.isValid())
|
||||
// {
|
||||
// mLog.info("TS:" + ((DMRMessage)message).getTimeslot() + " " + message.toString());
|
||||
// }
|
||||
}
|
||||
|
||||
public boolean hasData()
|
||||
{
|
||||
return mHasDMRData;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mHasDMRData = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LrrpProcessor implements Listener<IMessage>
|
||||
private void dispatchBufferA()
|
||||
{
|
||||
private Map<Integer, List<LRRPPacket>> mLrrpMap = new HashMap<>();
|
||||
mDibitCounter -= 144;
|
||||
|
||||
public LrrpProcessor()
|
||||
if(mDibitCounter > 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message instanceof DMRPacketMessage dpm)
|
||||
//If there are 144 dibits then tag the sync loss to other timeslot, otherwise dump it to timeslot 0.
|
||||
if(mDibitCounter == 144)
|
||||
{
|
||||
if(dpm.getPacket() instanceof LRRPPacket lrrp)
|
||||
//Note: since buffer b got skipped, we'll use buffer a timeslot and then it will get reassigned from
|
||||
//the CACH to the correct timeslot.
|
||||
dispatch(new SyncLossMessage(getTimestamp(), 288, Protocol.DMR, mBufferATimeslot));
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatch(new SyncLossMessage(getTimestamp(), mDibitCounter * 2, Protocol.DMR, DMRMessage.TIMESLOT_0));
|
||||
|
||||
if(mDibitCounter > 1 && !mBufferAPattern.isDirect())
|
||||
{
|
||||
process(lrrp);
|
||||
mBufferATimeslot = 0;
|
||||
mBufferBTimeslot = 0;
|
||||
mBufferBPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void process(LRRPPacket lrrpPacket)
|
||||
mDibitCounter = 0;
|
||||
|
||||
CorrectedBinaryMessage message = getMessage(mBufferA, DIBIT_CACH_START, DIBIT_BURST_END);
|
||||
CACH cach = CACH.getCACH(message);
|
||||
|
||||
if(mBufferAPattern.hasCACH() && cach.isValid() && mBufferATimeslot != cach.getTimeslot())
|
||||
{
|
||||
Identity identity = getIdentity(lrrpPacket);
|
||||
|
||||
if(identity != null)
|
||||
{
|
||||
List<LRRPPacket> packets = mLrrpMap.get(identity.getID());
|
||||
|
||||
if(packets == null)
|
||||
{
|
||||
packets = new ArrayList<>();
|
||||
}
|
||||
|
||||
packets.add(lrrpPacket);
|
||||
|
||||
mLrrpMap.put(identity.getID(), packets);
|
||||
}
|
||||
mBufferATimeslot = cach.getTimeslot();
|
||||
mBufferBTimeslot = (mBufferATimeslot == 1 ? 2 : 1);
|
||||
}
|
||||
|
||||
public void log()
|
||||
dispatch(DMRMessageFactory.create(mBufferAPattern, message, cach, getTimestamp(), mBufferATimeslot));
|
||||
|
||||
//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.
|
||||
if(mBufferAPattern.isVoicePattern())
|
||||
{
|
||||
mLog.info("*************** DUMPING LRRP PACKETS****************************");
|
||||
List<Integer> ids = new ArrayList(mLrrpMap.keySet());
|
||||
Collections.sort(ids);
|
||||
|
||||
for(Integer id: ids)
|
||||
{
|
||||
mLog.info("");
|
||||
mLog.info("###################### ID: " + id + " ###################################");
|
||||
List<LRRPPacket> packets = mLrrpMap.get(id);
|
||||
|
||||
for(LRRPPacket lrrp: packets)
|
||||
{
|
||||
mLog.info("PACKET: " + lrrp.toString());
|
||||
mLog.info("MESSAGE: " + lrrp.getMessage().toHexString());
|
||||
}
|
||||
}
|
||||
mBufferAPattern = DMRSyncPattern.getNextVoice(mBufferAPattern);
|
||||
}
|
||||
|
||||
private static Identity getIdentity(LRRPPacket packet)
|
||||
//Automatically trigger buffer B burst collection if it is collecting a voice super frame.
|
||||
if(mBufferBPattern.isVoicePattern())
|
||||
{
|
||||
for(Token token: packet.getTokens())
|
||||
{
|
||||
if(token instanceof Identity)
|
||||
{
|
||||
return (Identity)token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String path = "/media/denny/T7 Shield/Recordings/DMR/";
|
||||
|
||||
//Con+ Traffic + Voice
|
||||
// String file = path + "20200513_143340_9600BPS_DMR_SaiaNet_Onondaga_Control.bits"; //Enh GPS Revert Window Annce
|
||||
// String file = path + "20200514_062135_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Neighbor only
|
||||
// String file = path + "20200514_063507_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 2579
|
||||
// String file = path + "20200514_064224_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 5056035
|
||||
// String file = path + "20200514_131623_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Grant: 5074193
|
||||
// String file = path + "SaiaNet/20200514_133947_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; //<<<<<<<------ Basic Encryption
|
||||
// String file = path + "20200514_142249_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits";
|
||||
// String file = path + "20200514_144534_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Con+ Control w/GPS Window Announce
|
||||
|
||||
//Con+ Multi-Site with Network Frequency Map - This file is really General Motors Con+ - Tarrant County site
|
||||
// String file = path + "20200716_235004_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
|
||||
|
||||
//Cap-Max Tier III CC
|
||||
// String file = path + "20200710_053632_9600BPS_DMR_Niles_Radio_Coconino_Control.bits";
|
||||
|
||||
//Cap+ Multi-Site 1 - Data Revert LSN1/2 with CSBKO 62 Site status
|
||||
String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_210133_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
|
||||
// String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_210845_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
|
||||
// String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_212309_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
|
||||
|
||||
//Cap+ Multi-Site Enhanced Data Revert Channel (no CSBKO 62)
|
||||
// String file = path + "Texas_Hunt_Farmers_Electric_Capacity_Plus_Multi_Site/20200714_224018_9600BPS_DMR_Farmers_Electric_Cooperative_Hunt_LCN_3.bits"; //This may have PLL mis-align issues
|
||||
|
||||
//Cap+ Multi-Site - Dallas Unknown
|
||||
// String file = path + "Texas_Dallas_Unknown_1_Capacity_Plus_Multi_Site/20200716_211233_9600BPS_DMR_Dallas_Unk_Dallas_Unk.bits";
|
||||
|
||||
//Hytera Short Data = Proprietary (encrypted)
|
||||
// String file = path + "20200716_222839_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits"; //Hytera short data packets
|
||||
// String file = path + "20200716_223551_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits";
|
||||
|
||||
//Motorola GPS Revert channel
|
||||
// String file = path + "20200730_085433_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits";
|
||||
|
||||
//Hytera XPT (pseudo trunk) beaconing and voice
|
||||
// String file = path + "20200729_161450_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //PLL lock oscillating
|
||||
// String file = path + "20200729_162200_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits";
|
||||
// String file = path + "Hytera XPT/20200816_161126_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //analzing this <<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
//Hytera Tier III - CSBKO 40
|
||||
// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
|
||||
// String file = path + "20200812_061410_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
|
||||
// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; //MBC-channel grant
|
||||
|
||||
//UDT Header And Blocks
|
||||
// String file = path + "20200730_090634_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits";
|
||||
|
||||
//Cap+ BP Scrambling
|
||||
// String file = path + "20200829_065610_9600BPS_DMR_Albany_Medical_Center_Albany_LCN_3.bits";
|
||||
|
||||
// String file = "/home/denny/SDRTrunk/recordings/20200927_054837_9600BPS_DMR_Blair_Communications_(Capacity_Plus)_Dallas_Control_9.bits";
|
||||
|
||||
DecodeConfigDMR config = new DecodeConfigDMR();
|
||||
|
||||
Broadcaster<IMessage> messageBroadcaster = new Broadcaster<>();
|
||||
MessageListener messageListener = new MessageListener();
|
||||
messageBroadcaster.addListener(messageListener);
|
||||
|
||||
LrrpProcessor lrrpProcessor = new LrrpProcessor();
|
||||
messageBroadcaster.addListener(lrrpProcessor);
|
||||
|
||||
DMRCallSequenceRecorder ambeRecorder = new DMRCallSequenceRecorder(new UserPreferences(), 123456789l,
|
||||
"Denny System", "Denny Site");
|
||||
messageBroadcaster.addListener(ambeRecorder);
|
||||
|
||||
boolean multi = false;
|
||||
|
||||
if(multi)
|
||||
{
|
||||
try
|
||||
{
|
||||
DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(path), "*.bits");
|
||||
|
||||
stream.forEach(new Consumer<Path>()
|
||||
{
|
||||
@Override
|
||||
public void accept(Path path)
|
||||
{
|
||||
mLog.info("Processing: " + path.toString());
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
||||
messageFramer.setListener(messageProcessor);
|
||||
messageProcessor.setMessageListener(messageBroadcaster);
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(path, 200))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
messageFramer.receive(buffer);
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
if(!messageListener.hasData())
|
||||
{
|
||||
// mLog.info("Has Data: " + listener.hasData() + " File:" + path.toString());
|
||||
// try
|
||||
// {
|
||||
// Files.delete(path);
|
||||
// }
|
||||
// catch(IOException ioe)
|
||||
// {
|
||||
// ioe.printStackTrace();
|
||||
// }
|
||||
}
|
||||
|
||||
messageListener.reset();
|
||||
|
||||
System.out.println("TS0 VOICE:" + messageListener.mTS0Count + " TS1 VOICE:" + messageListener.mTS1Count);
|
||||
messageListener.mTS0Count = 0;
|
||||
messageListener.mTS1Count = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
lrrpProcessor.log();
|
||||
mAssemblingBurst = true;
|
||||
mBufferAActive = false;
|
||||
mBufferBPointer = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
||||
messageFramer.setListener(messageProcessor);
|
||||
messageProcessor.setMessageListener(messageBroadcaster);
|
||||
mAssemblingBurst = false;
|
||||
}
|
||||
}
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(Path.of(file), 200))
|
||||
private void dispatchBufferB()
|
||||
{
|
||||
mDibitCounter -= 144;
|
||||
|
||||
if(mDibitCounter > 0)
|
||||
{
|
||||
//If there are 144 dibits then tag the sync loss to other timeslot, otherwise dump it to timeslot 0.
|
||||
if(mDibitCounter == 144)
|
||||
{
|
||||
while(reader.hasNext())
|
||||
//Note: since buffer a got skipped, we'll use buffer b timeslot and then it will get reassigned from
|
||||
//the CACH to the correct timeslot.
|
||||
dispatch(new SyncLossMessage(getTimestamp(), 288, Protocol.DMR, mBufferBTimeslot));
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatch(new SyncLossMessage(getTimestamp(), mDibitCounter * 2, Protocol.DMR, DMRMessage.TIMESLOT_0));
|
||||
|
||||
if(mDibitCounter > 1 && !mBufferBPattern.isDirect())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
messageFramer.receive(buffer);
|
||||
mBufferATimeslot = 0;
|
||||
mBufferBTimeslot = 0;
|
||||
mBufferAPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("TS0 VOICE:" + messageListener.mTS0Count + " TS1 VOICE:" + messageListener.mTS1Count);
|
||||
messageListener.mTS0Count = 0;
|
||||
messageListener.mTS1Count = 0;
|
||||
mDibitCounter = 0;
|
||||
|
||||
CorrectedBinaryMessage burst = getMessage(mBufferB, DIBIT_CACH_START, DIBIT_BURST_END);
|
||||
CACH cach = CACH.getCACH(burst);
|
||||
|
||||
if(mBufferBPattern.hasCACH() && cach.isValid() && mBufferBTimeslot != cach.getTimeslot())
|
||||
{
|
||||
mBufferBTimeslot = cach.getTimeslot();
|
||||
mBufferATimeslot = (mBufferBTimeslot == 1 ? 2 : 1);
|
||||
}
|
||||
|
||||
dispatch(DMRMessageFactory.create(mBufferBPattern, burst, cach, getTimestamp(), mBufferBTimeslot));
|
||||
|
||||
//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.
|
||||
if(mBufferBPattern.isVoicePattern())
|
||||
{
|
||||
mBufferBPattern = DMRSyncPattern.getNextVoice(mBufferBPattern);
|
||||
}
|
||||
|
||||
//Automatically trigger buffer A burst collection if it is collecting a voice super frame.
|
||||
if(mBufferAPattern.isVoicePattern())
|
||||
{
|
||||
mAssemblingBurst = true;
|
||||
mBufferAActive = true;
|
||||
mBufferAPointer = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
mAssemblingBurst = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the message to the registered message listener.
|
||||
* @param message
|
||||
*/
|
||||
private void dispatch(IMessage message)
|
||||
{
|
||||
if(mRunning && mMessageListener != null)
|
||||
{
|
||||
mMessageListener.receive(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this framer dispatching messages
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
mRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this framer from dispatching messages
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to receive framed DMR messages.
|
||||
* @param listener for messages.
|
||||
*/
|
||||
public void setListener(Listener<IMessage> listener)
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a binary message from the dibit buffer.
|
||||
* @param buffer containing dibits
|
||||
* @param start index inclusive
|
||||
* @param end index exclusive.
|
||||
* @return binary message
|
||||
*/
|
||||
private CorrectedBinaryMessage getMessage(Dibit[] buffer, int start, int end)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(2 * (end - start));
|
||||
|
||||
Dibit dibit = null;
|
||||
for(int i = start; i < end; i++)
|
||||
{
|
||||
dibit = buffer[i];
|
||||
message.add(dibit.getBit1(), dibit.getBit2());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Externally triggered sync detection to initiate DMR burst capture. Indicates a sync pattern is detected and the
|
||||
* next received dibit symbol is the first dibit of the DMR burst.
|
||||
* @param pattern detected
|
||||
*/
|
||||
public void syncDetected(DMRSyncPattern pattern)
|
||||
{
|
||||
if(mAssemblingBurst)
|
||||
{
|
||||
if(mBufferAActive && mBufferAPointer < mBufferA.length)
|
||||
{
|
||||
mBufferAPointer = 144;
|
||||
mBufferAPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
else if(!mBufferAActive && mBufferBPointer < mBufferB.length)
|
||||
{
|
||||
mBufferBPointer = 144;
|
||||
mBufferBPattern = DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
mAssemblingBurst = true;
|
||||
|
||||
if(mBufferAActive)
|
||||
{
|
||||
mBufferAActive = false;
|
||||
mBufferBPointer = 0;
|
||||
mBufferBPattern = pattern;
|
||||
|
||||
if(pattern.isDirect())
|
||||
{
|
||||
if(pattern.isDirectTS1())
|
||||
{
|
||||
mBufferATimeslot = 2;
|
||||
mBufferBTimeslot = 1;
|
||||
}
|
||||
else if(pattern.isDirectTS2())
|
||||
{
|
||||
mBufferATimeslot = 1;
|
||||
mBufferBTimeslot = 2;
|
||||
}
|
||||
|
||||
if(mBufferAPattern == DMRSyncPattern.UNKNOWN)
|
||||
{
|
||||
mBufferAPattern = DMRSyncPattern.DIRECT_EMPTY_TIMESLOT;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mBufferAActive = true;
|
||||
mBufferAPointer = 0;
|
||||
mBufferAPattern = pattern;
|
||||
|
||||
if(pattern.isDirect())
|
||||
{
|
||||
if(pattern.isDirectTS1())
|
||||
{
|
||||
mBufferATimeslot = 1;
|
||||
mBufferBTimeslot = 2;
|
||||
}
|
||||
else if(pattern.isDirectTS2())
|
||||
{
|
||||
mBufferATimeslot = 2;
|
||||
mBufferBTimeslot = 1;
|
||||
}
|
||||
|
||||
if(mBufferBPattern == DMRSyncPattern.UNKNOWN)
|
||||
{
|
||||
mBufferBPattern = DMRSyncPattern.DIRECT_EMPTY_TIMESLOT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates the current dibit stream time from an incoming sample buffer.
|
||||
* @param time to use as a reference timestamp.
|
||||
*/
|
||||
public void setTimestamp(long time)
|
||||
{
|
||||
mReferenceTimestamp = time;
|
||||
mDibitSinceTimestampCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the timestamp accurate to the currently received dibit.
|
||||
* @return timestamp in milliseconds.
|
||||
*/
|
||||
private long getTimestamp()
|
||||
{
|
||||
if(mReferenceTimestamp > 0)
|
||||
{
|
||||
return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / 4800);
|
||||
}
|
||||
else
|
||||
{
|
||||
mDibitSinceTimestampCounter = 0;
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.SyncLossMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
@@ -73,7 +74,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
||||
private Listener<IMessage> mMessageListener;
|
||||
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
||||
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
|
||||
private DMRCrcMaskManager mCrcMaskManager = new DMRCrcMaskManager();
|
||||
private boolean mIgnoreCrcChecksums;
|
||||
|
||||
/**
|
||||
@@ -115,6 +116,13 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if(message instanceof SyncLossMessage)
|
||||
{
|
||||
mSLCAssembler.reset();
|
||||
mFLCAssemblerTimeslot1.reset();
|
||||
mFLCAssemblerTimeslot2.reset();
|
||||
}
|
||||
|
||||
//Detect and correct messages employing an alternate CRC mask pattern.
|
||||
if(!message.isValid() && message instanceof CSBKMessage csbk)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.PhaseAwareLinearInterpolator;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 three soft sync correlation detectors to achieve initial sync lock with a primary sync detector and two lagging
|
||||
* sync detectors sampling at 1/3 and 2/3 of the symbol period to ensure the sync is detected while only calculating at
|
||||
* each symbol period interval. Once a sync is detected, sync lock is declared and the lagging sync detectors are
|
||||
* disabled. Sync lock disables whenever the quantity of processed symbols exceeds one DMR burst length and the
|
||||
* lagging detectors re-enable.
|
||||
*
|
||||
* 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]
|
||||
*/
|
||||
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 MAX_POSITIVE_SOFT_SYMBOL = Dibit.D01_PLUS_3.getIdealPhase();
|
||||
private static final float MAX_NEGATIVE_SOFT_SYMBOL = Dibit.D11_MINUS_3.getIdealPhase();
|
||||
private static final float SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
|
||||
private static final float SYNC_DETECTION_THRESHOLD = 60;
|
||||
private DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
|
||||
private DMRSoftSyncDetector mSyncDetector = DMRSoftSyncDetectorFactory.getDetector();
|
||||
private DMRSoftSyncDetector mSyncDetectorLag1 = DMRSoftSyncDetectorFactory.getDetector();
|
||||
private DMRSoftSyncDetector mSyncDetectorLag2 = 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 mSyncLock = false;
|
||||
private float mLaggingSyncOffset1;
|
||||
private float mLaggingSyncOffset2;
|
||||
private float mObservedSamplesPerSymbol;
|
||||
private float mSamplesPerSymbol;
|
||||
private float mSamplePoint;
|
||||
private float[] mBuffer;
|
||||
private int mBufferLoadPointer;
|
||||
private int mBufferPointer;
|
||||
private int mBufferWorkspaceLength;
|
||||
private int mSymbolsSinceLastSync = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param messageFramer to receive demodulated symbols/dibits and sync detect notifications
|
||||
*/
|
||||
public DMRSoftSymbolProcessor(DMRMessageFramer messageFramer)
|
||||
{
|
||||
if(messageFramer == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Message framer cannot be null");
|
||||
}
|
||||
|
||||
mMessageFramer = messageFramer;
|
||||
mSyncModeMonitor.add(mSyncDetector);
|
||||
mSyncModeMonitor.add(mSyncDetectorLag1);
|
||||
mSyncModeMonitor.add(mSyncDetectorLag2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 baseStationMode true for base mode only, false otherwise.
|
||||
*/
|
||||
public void setBaseStationMode(boolean baseStationMode)
|
||||
{
|
||||
if(baseStationMode)
|
||||
{
|
||||
mSyncModeMonitor.setMode(DMRSyncDetectMode.BASE_ONLY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
while(mBufferPointer < (mBufferLoadPointer - 7)) //Interpolator needs 1 and optimizer needs 6 pad spaces
|
||||
{
|
||||
mBufferPointer++;
|
||||
mSamplePoint--;
|
||||
|
||||
if(mSamplePoint < 1)
|
||||
{
|
||||
mSymbolsSinceLastSync++;
|
||||
|
||||
if(mSymbolsSinceLastSync > 144)
|
||||
{
|
||||
mSyncLock = false;
|
||||
}
|
||||
|
||||
float softSymbol = PhaseAwareLinearInterpolator.calculate(mBuffer[mBufferPointer],
|
||||
mBuffer[mBufferPointer + 1], mSamplePoint);
|
||||
|
||||
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);
|
||||
|
||||
float scorePrimary = mSyncDetector.process(softSymbol);
|
||||
|
||||
if(mSyncLock && scorePrimary > SYNC_DETECTION_THRESHOLD &&
|
||||
optimize(mSyncDetector.getDetectedPattern(), 0.0f))
|
||||
{
|
||||
mSymbolsSinceLastSync = 0;
|
||||
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
|
||||
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
|
||||
}
|
||||
else
|
||||
{
|
||||
float lag1 = mBufferPointer + mSamplePoint - mLaggingSyncOffset1;
|
||||
float lag2 = mBufferPointer + mSamplePoint - mLaggingSyncOffset2;
|
||||
int lagIntegral1 = (int)Math.floor(lag1);
|
||||
int lagIntegral2 = (int)Math.floor(lag2);
|
||||
float softSymbolLag1 = PhaseAwareLinearInterpolator.calculate(mBuffer[lagIntegral1],
|
||||
mBuffer[lagIntegral1 + 1], lag1 - lagIntegral1);
|
||||
float softSymbolLag2 = PhaseAwareLinearInterpolator.calculate(mBuffer[lagIntegral2],
|
||||
mBuffer[lagIntegral2 + 1], lag2 - lagIntegral2);
|
||||
float scoreLag1 = mSyncDetectorLag1.process(softSymbolLag1);
|
||||
float scoreLag2 = mSyncDetectorLag2.process(softSymbolLag2);
|
||||
|
||||
if(mSymbolsSinceLastSync > 1 && scoreLag1 > scorePrimary && scoreLag1 > scoreLag2 &&
|
||||
scoreLag1 > SYNC_DETECTION_THRESHOLD &&
|
||||
optimize(mSyncDetectorLag1.getDetectedPattern(), -mLaggingSyncOffset1))
|
||||
{
|
||||
mSymbolsSinceLastSync = 0;
|
||||
mSyncModeMonitor.detected(mSyncDetectorLag1.getDetectedPattern());
|
||||
mMessageFramer.syncDetected(mSyncDetectorLag1.getDetectedPattern());
|
||||
}
|
||||
else if(mSymbolsSinceLastSync > 1 && scoreLag2 > scorePrimary &&
|
||||
scoreLag2 > SYNC_DETECTION_THRESHOLD &&
|
||||
optimize(mSyncDetectorLag2.getDetectedPattern(), -mLaggingSyncOffset2))
|
||||
{
|
||||
mSymbolsSinceLastSync = 0;
|
||||
mSyncModeMonitor.detected(mSyncDetectorLag2.getDetectedPattern());
|
||||
mMessageFramer.syncDetected(mSyncDetectorLag2.getDetectedPattern());
|
||||
}
|
||||
else if(scorePrimary > SYNC_DETECTION_THRESHOLD &&
|
||||
optimize(mSyncDetector.getDetectedPattern(), 0.0f))
|
||||
{
|
||||
mSymbolsSinceLastSync = 0;
|
||||
mSyncLock = true;
|
||||
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
|
||||
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
|
||||
}
|
||||
}
|
||||
|
||||
//Add another symbol's worth of samples to the counter
|
||||
mSamplePoint += mObservedSamplesPerSymbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 optimize(DMRSyncPattern pattern, float 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.
|
||||
float offset = (mBufferPointer + mSamplePoint) + additionalOffset - (mObservedSamplesPerSymbol * 23);
|
||||
|
||||
//Find the optimal symbol timing
|
||||
float stepSize = mObservedSamplesPerSymbol / 10.0f;
|
||||
float stepSizeMin = 0.03f;
|
||||
float adjustment = 0.0f;
|
||||
float adjustmentMax = mObservedSamplesPerSymbol / 2.0f;
|
||||
float candidate = offset;
|
||||
|
||||
int candidateIntegral = (int)Math.floor(candidate);
|
||||
float candidateFractional = candidate - candidateIntegral;
|
||||
float scoreCenter = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
|
||||
candidate = offset - stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
float scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
|
||||
candidate = offset + stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
float scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
|
||||
while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax)
|
||||
{
|
||||
if(scoreLeft > scoreRight && scoreLeft > scoreCenter)
|
||||
{
|
||||
adjustment -= stepSize;
|
||||
scoreRight = scoreCenter;
|
||||
scoreCenter = scoreLeft;
|
||||
|
||||
candidate = offset + adjustment - stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
}
|
||||
else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
|
||||
{
|
||||
adjustment += stepSize;
|
||||
scoreLeft = scoreCenter;
|
||||
scoreCenter = scoreRight;
|
||||
|
||||
candidate = offset + adjustment + stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
}
|
||||
else
|
||||
{
|
||||
stepSize *= 0.5f;
|
||||
|
||||
if(stepSize > stepSizeMin)
|
||||
{
|
||||
candidate = offset + adjustment - stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
|
||||
candidate = offset + adjustment + stepSize;
|
||||
candidateIntegral = (int)Math.floor(candidate);
|
||||
candidateFractional = candidate - candidateIntegral;
|
||||
scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we didn't find an optimal correlation score above the 95 threshold, return a false sync.
|
||||
if(scoreCenter < 95)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(additionalOffset != 0.0)
|
||||
{
|
||||
adjustment += additionalOffset;
|
||||
}
|
||||
|
||||
mSamplePoint += adjustment;
|
||||
|
||||
while(mSamplePoint < 0)
|
||||
{
|
||||
mSamplePoint++;
|
||||
mBufferPointer--;
|
||||
}
|
||||
|
||||
while(mSamplePoint > 1)
|
||||
{
|
||||
mSamplePoint--;
|
||||
mBufferPointer++;
|
||||
}
|
||||
|
||||
//Adjust the observed samples per symbol using the timing error measured across one or two bursts when we're in
|
||||
//fine sync mode and the timing error is not excessive.
|
||||
if(mSyncLock && adjustment < 0.5 && mSymbolsSinceLastSync > 143 && mSymbolsSinceLastSync < 289)
|
||||
{
|
||||
mObservedSamplesPerSymbol += (float)((double)adjustment / (double)mSymbolsSinceLastSync * 0.2);
|
||||
}
|
||||
|
||||
//If 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(Math.abs(adjustment) > 0.05)
|
||||
{
|
||||
float resampleStart = mBufferPointer + mSamplePoint;
|
||||
resampleStart -= (89 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
|
||||
int resampleStartIntegral;
|
||||
|
||||
for(int x = 0; x < 66; x++)
|
||||
{
|
||||
resampleStartIntegral = (int)Math.floor(resampleStart);
|
||||
|
||||
if(resampleStartIntegral >= 0)
|
||||
{
|
||||
float resampledSoftSymbol = PhaseAwareLinearInterpolator.calculate(mBuffer[resampleStartIntegral],
|
||||
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
|
||||
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);
|
||||
}
|
||||
|
||||
resampleStart += 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sync correlation score for sync pattern at the specified offsets and symbol timing.
|
||||
* @param bufferPointer to the start of the samples in the soft symbol buffer
|
||||
* @param fractional position to interpolate within the 8 samples starting at the buffer pointer.
|
||||
* @param samplesPerSymbol spacing to test for.
|
||||
* @param pattern to correlate against.
|
||||
* @return correlation score.
|
||||
*/
|
||||
public float score(int bufferPointer, float fractional, float samplesPerSymbol, DMRSyncPattern pattern)
|
||||
{
|
||||
float score = 0;
|
||||
float[] symbols = pattern.toSymbols();
|
||||
float[] softSymbols = new float[24];
|
||||
int integral;
|
||||
|
||||
for(int x = 0; x < 24; x++)
|
||||
{
|
||||
float softSymbol = PhaseAwareLinearInterpolator.calculate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional);
|
||||
softSymbol = Math.min(softSymbol, MAX_POSITIVE_SOFT_SYMBOL);
|
||||
softSymbol = Math.max(softSymbol, MAX_NEGATIVE_SOFT_SYMBOL);
|
||||
softSymbols[x] = softSymbol;
|
||||
score += softSymbol * symbols[x];
|
||||
fractional += samplesPerSymbol;
|
||||
integral = (int)Math.floor(fractional);
|
||||
bufferPointer += integral;
|
||||
fractional -= integral;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates the samples per symbol
|
||||
* @param samplesPerSymbol to apply.
|
||||
*/
|
||||
public void setSamplesPerSymbol(float samplesPerSymbol)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mObservedSamplesPerSymbol = mSamplesPerSymbol;
|
||||
mSamplePoint = mSamplesPerSymbol;
|
||||
mLaggingSyncOffset1 = mSamplesPerSymbol / 3;
|
||||
mLaggingSyncOffset2 = mLaggingSyncOffset1 * 2;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.QPSKCarrierLock;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
/**
|
||||
* Detector for DMR sync patterns that also includes support for detecting PLL phase lock errors. There are
|
||||
* two primary use cases for this class:
|
||||
*
|
||||
* The first is to detect a sync pattern from a stream of dibits that are processed via the add(dibit) method. After
|
||||
* each dibit is added, the sync pattern can be inspected by either the hasSync() method or the getSyncPattern()
|
||||
* method. The maxStreamBitErrors construction parameter specifies how many bit errors can be present when matching
|
||||
* patterns to the dibit stream.
|
||||
*
|
||||
* The second use case is to set a candidate sync pattern value using the setCurrentSyncValue() method. You can then
|
||||
* use the hasSync() and getSyncPatter() methods to determine if the set value matches a valid sync pattern. The
|
||||
* maxExplicitBitErrors specifies how many bit errors can be present when matching patterns to the explicitly set
|
||||
* sync value.
|
||||
*
|
||||
* Note: sync pattern matching maximum bit errors uses a hard-coded value for PLL misalignment sync detection since
|
||||
* this threshold should be relatively high to avoid false triggering and once detected, a PLL should be immediately
|
||||
* commanded to correct the issue.
|
||||
*/
|
||||
public class DMRSyncDetector
|
||||
{
|
||||
private static long SYNC_MASK = 0xFFFFFFFFFFFFl;
|
||||
private static final int MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT = 1;
|
||||
private int mMaxStreamBitErrors;
|
||||
private int mMaxExplicitBitErrors;
|
||||
private long mCurrentSyncValue;
|
||||
private long mErrorPattern;
|
||||
private int mPatternMatchBitErrorCount;
|
||||
private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
|
||||
private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param maxStreamBitErrors allowed for matching sync patterns when using the dibit streaming use case.
|
||||
* @param maxExplicitBitErrors allows for matching sync patterns when using the explicitly set sync value use case.
|
||||
*/
|
||||
public DMRSyncDetector(int maxStreamBitErrors, int maxExplicitBitErrors)
|
||||
{
|
||||
Validate.inclusiveBetween(0, 24, maxStreamBitErrors,
|
||||
"Max (allowable) stream bit errors for sync match must be between 0 and 24");
|
||||
Validate.inclusiveBetween(0, 24, maxExplicitBitErrors,
|
||||
"Max (allowable) explicit bit errors for sync match must be between 0 and 24");
|
||||
mMaxStreamBitErrors = maxStreamBitErrors;
|
||||
mMaxExplicitBitErrors = maxExplicitBitErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync pattern for the current sync value.
|
||||
*/
|
||||
public DMRSyncPattern getSyncPattern()
|
||||
{
|
||||
return mSyncPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there is a sync pattern in the currently set sync value that was either explicitly set or was
|
||||
* aggregated from a stream of dibits. When this method returns true, the sync pattern can be accessed via the
|
||||
* getSyncPattern() method.
|
||||
*/
|
||||
public boolean hasSync()
|
||||
{
|
||||
return getSyncPattern() != DMRSyncPattern.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrier lock for the QPSK PLL to detect when the PLL has locked to the signal at +/- 90 degrees or 180 degrees.
|
||||
* @return carrier lock value.
|
||||
*/
|
||||
public QPSKCarrierLock getCarrierLock()
|
||||
{
|
||||
return mCarrierLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the detected sync pattern has normal QPSK carrier lock.
|
||||
*/
|
||||
public boolean hasNormalCarrierLock()
|
||||
{
|
||||
return getCarrierLock() == QPSKCarrierLock.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of bit errors that were present when matching the current sync value to the current sync pattern.
|
||||
*/
|
||||
public int getPatternMatchBitErrorCount()
|
||||
{
|
||||
return mPatternMatchBitErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the streaming dibit by left shifting the current sync value and adding the dibit onto the end and then
|
||||
* test for sync pattern match.
|
||||
* @param dibit to process and test for sync pattern.
|
||||
*/
|
||||
public void add(Dibit dibit)
|
||||
{
|
||||
mCurrentSyncValue = Long.rotateLeft(mCurrentSyncValue, 2);
|
||||
mCurrentSyncValue &= SYNC_MASK;
|
||||
mCurrentSyncValue += dibit.getValue();
|
||||
checkSync(mMaxStreamBitErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly sets the argument as the current sync value and tests for sync pattern match.
|
||||
* @param value to load as the current sync value and test
|
||||
*/
|
||||
public void setCurrentSyncValue(long value)
|
||||
{
|
||||
mCurrentSyncValue = value;
|
||||
checkSync(mMaxExplicitBitErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current sync value against each of the sync patterns to determine if the value matches a pattern.
|
||||
*/
|
||||
private void checkSync(int maxBitErrors)
|
||||
{
|
||||
for(DMRSyncPattern pattern: DMRSyncPattern.SYNC_PATTERNS)
|
||||
{
|
||||
mErrorPattern = mCurrentSyncValue ^ pattern.getPattern();
|
||||
|
||||
if(mErrorPattern == 0)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = QPSKCarrierLock.NORMAL;
|
||||
mPatternMatchBitErrorCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
mPatternMatchBitErrorCount = Long.bitCount(mErrorPattern);
|
||||
|
||||
if(mPatternMatchBitErrorCount <= maxBitErrors)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = QPSKCarrierLock.NORMAL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//For PLL mis-aligned lock patterns, reduce the pattern bit error match threshold
|
||||
mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getPlus90Pattern());
|
||||
|
||||
if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = QPSKCarrierLock.PLUS_90;
|
||||
return;
|
||||
}
|
||||
|
||||
mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getMinus90Pattern());
|
||||
|
||||
if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = QPSKCarrierLock.MINUS_90;
|
||||
return;
|
||||
}
|
||||
|
||||
mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getInvertedPattern());
|
||||
|
||||
if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
|
||||
{
|
||||
mSyncPattern = pattern;
|
||||
mCarrierLock = QPSKCarrierLock.INVERTED;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mSyncPattern = DMRSyncPattern.UNKNOWN;
|
||||
mCarrierLock = QPSKCarrierLock.NORMAL;
|
||||
mPatternMatchBitErrorCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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 java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* DMR burst sync patterns enumeration. In addition to the ETSI defined sync patterns, each enumeration entry also
|
||||
* contains derived sync patterns that match decoded bit streams where a QPSK Phase-Locked Loop has locked to the signal
|
||||
* misaligned by +/- 90 degrees, or 180 degrees. These derived patterns can be used to detect PLL misalignment for
|
||||
* PLL correction purposes.
|
||||
*/
|
||||
public enum DMRSyncPattern
|
||||
{
|
||||
BASE_STATION_DATA(0xDFF57D75DF5Dl, 0xBAAFEBEFBAFBl, 0x455014104504l, 0x200A828A20A2l, "BS DATA"),
|
||||
BASE_STATION_VOICE(0x755FD7DF75F7l, 0xEFFABEBAEFAEl, 0x100541451051l, 0x8AA028208A08l, "BS VOICE A"),
|
||||
|
||||
MOBILE_STATION_DATA(0xD5D7F77FD757l, 0xBFBEAEEABEFEl, 0x404151154101l, 0x2A28088028A8l, "MS DATA"),
|
||||
MOBILE_STATION_VOICE(0x7F7D5DD57DFDl, 0xEAEBFBBFEBABl, 0x151404401454l, 0x8082A22A8202l, "MS VOICE A"),
|
||||
DIRECT_MODE_DATA_TIMESLOT_1(0xF7FDD5DDFD55l, 0xAEABBFBBABFFl, 0x515440445400l, 0x08022A2202AAl, "MS DATA - DUAL CAPACITY DIRECT MODE"),
|
||||
DIRECT_MODE_DATA_TIMESLOT_2(0xD7557F5FF7F5l, 0xBEFFEAFAAEAFl, 0x410015055150l, 0x28AA80A0080Al, "MS DATA - DUAL CAPACITY DIRECT MODE"),
|
||||
DIRECT_MODE_VOICE_TIMESLOT_1(0x5D577F7757FFl, 0xFBFEEAEEFEAAl, 0x040115110155l, 0xA2A88088A800l, "MS VOICE A - DUAL CAPACITY DIRECT MODE"),
|
||||
DIRECT_MODE_VOICE_TIMESLOT_2(0x7DFFD5F55D5Fl, 0xEBAABFAFFBFAl, 0x145540500405l, 0x82002A0AA2A0l, "MS VOICE A - DUAL CAPACITY DIRECT MODE"),
|
||||
MOBILE_STATION_REVERSE_CHANNEL(0x77D55F7DFD77l, 0xEEBFFAEBABEEl, 0x114005145411l, 0x882AA0820288l, "MS RVRS"),
|
||||
|
||||
RESERVED(0xDD7FF5D757DDl, 0xBBEAAFBEFEBBl, 0x441550410144l, 0x22800A28A822l, "RESERVED"),
|
||||
|
||||
//These are used to identify the sync-less sub frames of a voice super frame
|
||||
BS_VOICE_FRAME_B("BS VOICE B"),
|
||||
BS_VOICE_FRAME_C("BS VOICE C"),
|
||||
BS_VOICE_FRAME_D("BS VOICE D"),
|
||||
BS_VOICE_FRAME_E("BS VOICE E"),
|
||||
BS_VOICE_FRAME_F("BS VOICE F"),
|
||||
|
||||
MS_VOICE_FRAME_B("MS VOICE B"),
|
||||
MS_VOICE_FRAME_C("MS VOICE C"),
|
||||
MS_VOICE_FRAME_D("MS VOICE D"),
|
||||
MS_VOICE_FRAME_E("MS VOICE E"),
|
||||
MS_VOICE_FRAME_F("MS VOICE F"),
|
||||
|
||||
UNKNOWN("UNKNOWN");
|
||||
|
||||
private long mPattern;
|
||||
private long mPlus90Pattern;
|
||||
private long mMinus90Pattern;
|
||||
private long mInvertedPattern;
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* DMR Sync Patterns. See TS 102-361-1, paragraph 9.1.1
|
||||
*/
|
||||
DMRSyncPattern(long pattern, long plus90Pattern, long minus90Pattern, long invertedPattern, String label)
|
||||
{
|
||||
mPattern = pattern;
|
||||
mPlus90Pattern = plus90Pattern;
|
||||
mMinus90Pattern = minus90Pattern;
|
||||
mInvertedPattern = invertedPattern;
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate constructor for syncs that don't have defined patterns.
|
||||
*/
|
||||
DMRSyncPattern(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
//Valid sync patterns (excluding the sync-less voice patterns)
|
||||
public static final EnumSet<DMRSyncPattern> SYNC_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
||||
MOBILE_STATION_DATA, MOBILE_STATION_VOICE, DIRECT_MODE_DATA_TIMESLOT_1, DIRECT_MODE_DATA_TIMESLOT_2,
|
||||
DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2, MOBILE_STATION_REVERSE_CHANNEL);
|
||||
|
||||
//Sync patterns containing a Common Announcement Channel (CACH)
|
||||
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
||||
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
||||
|
||||
//Direct Mode TS 1 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
|
||||
DIRECT_MODE_DATA_TIMESLOT_1);
|
||||
|
||||
//Direct Mode TS 2 Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS2_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_2,
|
||||
DIRECT_MODE_DATA_TIMESLOT_2);
|
||||
|
||||
//Mobile Station Sync Patterns
|
||||
private static final EnumSet<DMRSyncPattern> MOBILE_STATION_PATTERNS = EnumSet.of(MOBILE_STATION_DATA,
|
||||
MOBILE_STATION_VOICE, MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F);
|
||||
|
||||
/**
|
||||
* Pattern that represents the enum entry
|
||||
*/
|
||||
public long getPattern()
|
||||
{
|
||||
return mPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern that represents the PLL locking to the carrier misaligned by plus 90 degrees.
|
||||
*/
|
||||
public long getPlus90Pattern()
|
||||
{
|
||||
return mPlus90Pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern that represents the PLL locking to the carrier misaligned by minus 90 degrees.
|
||||
*/
|
||||
public long getMinus90Pattern()
|
||||
{
|
||||
return mMinus90Pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern that represents the PLL locking to the carrier misaligned by 180 degrees (inverted).
|
||||
*/
|
||||
public long getInvertedPattern()
|
||||
{
|
||||
return mInvertedPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a message using this sync pattern contains a Common Announcement Channel (CACH).
|
||||
*/
|
||||
public boolean hasCACH()
|
||||
{
|
||||
return CACH_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a Direct Mode sync pattern
|
||||
*/
|
||||
public boolean isDirectMode()
|
||||
{
|
||||
return isDirectModeTS1() || isDirectModeTS2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 1
|
||||
* @return true if DMTS1
|
||||
*/
|
||||
public boolean isDirectModeTS1()
|
||||
{
|
||||
return DIRECT_MODE_TS1_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is Direct Mode sync pattern for timestot 2
|
||||
* @return true if DMTS2
|
||||
*/
|
||||
public boolean isDirectModeTS2()
|
||||
{
|
||||
return DIRECT_MODE_TS2_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile station sync pattern
|
||||
* @return true if a mobile station sync pattern.
|
||||
*/
|
||||
public boolean isMobileStationSyncPattern()
|
||||
{
|
||||
return MOBILE_STATION_PATTERNS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a mobile sync pattern, either mobile station or direct mode.
|
||||
* @return true if a mobile sync pattern.
|
||||
*/
|
||||
public boolean isMobileSyncPattern()
|
||||
{
|
||||
return isMobileStationSyncPattern() || isDirectMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation of the sync pattern
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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.symbol.Dibit;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Implements a circular buffer delay line for dibits.
|
||||
*/
|
||||
public class DibitDelayLine
|
||||
{
|
||||
private Dibit[] mDelayLine;
|
||||
public int mPointer;
|
||||
private int mLength;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param length of the delay line in dibits.
|
||||
*/
|
||||
public DibitDelayLine(int length)
|
||||
{
|
||||
mLength = length;
|
||||
mDelayLine = new Dibit[length];
|
||||
Arrays.fill(mDelayLine, Dibit.D00_PLUS_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the dibit into the delay line and returns the oldest dibit from the delay line.
|
||||
* @param dibit to insert
|
||||
* @return oldest dibit
|
||||
*/
|
||||
public Dibit insert(Dibit dibit)
|
||||
{
|
||||
Dibit ejected = mDelayLine[mPointer];
|
||||
mDelayLine[mPointer++] = dibit;
|
||||
mPointer %= mLength;
|
||||
return ejected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the most recently added dibits in the delay line with the provided sequence. This can be used when
|
||||
* a sync pattern is detected to overwrite the calculated symbols with the known sync pattern.
|
||||
* @param dibits to overwrite
|
||||
*/
|
||||
public void update(Dibit[] dibits)
|
||||
{
|
||||
mPointer = mPointer - dibits.length;
|
||||
|
||||
if(mPointer < 0)
|
||||
{
|
||||
mPointer += mDelayLine.length;
|
||||
}
|
||||
|
||||
for(Dibit dibit: dibits)
|
||||
{
|
||||
mDelayLine[mPointer++] = dibit;
|
||||
mPointer %= mLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer, Zhenyu Mao
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Listener interface to be notified each time a DMR burst has been detected or when sync is or continues to be lost.
|
||||
*/
|
||||
public interface IDMRBurstDetectListener
|
||||
{
|
||||
/**
|
||||
* Indicates that a DMR sync has been detected and binary message with the burst contents are ready for processing
|
||||
* @param binaryMessage burst Binary Message and any bit errors detected in the message
|
||||
* @param pattern SyncPattern
|
||||
*/
|
||||
void burstDetected(CorrectedBinaryMessage binaryMessage, DMRSyncPattern pattern, int timeslot);
|
||||
|
||||
/**
|
||||
* Indicates that sync has been lost on the dibit stream
|
||||
*
|
||||
* @param bitsProcessed since the last sync detect
|
||||
* @param timeslot that was being tracked when the sync loss was detected
|
||||
*/
|
||||
void syncLost(int bitsProcessed, int timeslot);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.module.decode.dmr.message.CACH;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Tracks proper alignment of timeslots into the two bursts of a burst framer.
|
||||
*/
|
||||
public class TimeslotAlignmentTracker
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TimeslotAlignmentTracker.class);
|
||||
|
||||
private int mBurst1Ts1 = 0;
|
||||
private int mBurst1Ts2 = 0;
|
||||
private int mBurst2Ts1 = 0;
|
||||
private int mBurst2Ts2 = 0;
|
||||
|
||||
/**
|
||||
* Indicates if the track has accumulated enough cach data to accurately declare timeslot alignment. The test
|
||||
* checks that both timeslots each have different counts for timeslot 0 and 1 and that both timeslots each indicate
|
||||
* a different dominant timeslot than the other.
|
||||
*/
|
||||
public boolean hasSufficientData()
|
||||
{
|
||||
return ((mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts2 > mBurst2Ts1) ||
|
||||
(mBurst1Ts1 < mBurst1Ts2 && mBurst2Ts2 < mBurst2Ts1) ||
|
||||
(mBurst1Ts1 == 0 && mBurst1Ts2 == 0 && mBurst2Ts1 != mBurst2Ts2) ||
|
||||
(mBurst1Ts1 != mBurst1Ts2 && mBurst2Ts1 == 0 && mBurst1Ts2 == 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the timeslots are aligned by inspection of the CACH value for frame 0 and frame 1
|
||||
*/
|
||||
public boolean isAligned()
|
||||
{
|
||||
return (mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts2 > mBurst2Ts1) ||
|
||||
(mBurst1Ts1 == 0 && mBurst1Ts2 == 0 && mBurst2Ts2 > mBurst2Ts1) ||
|
||||
(mBurst1Ts1 > mBurst1Ts2 && mBurst2Ts1 == 0 && mBurst2Ts2 == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tracker with the cach fields or sync patterns from the current TDMA burst frames
|
||||
* @param cach1 optional cach that is currently aligned as timeslot 1
|
||||
* @param cach2 optional cach that is currently aligned as timeslot 2
|
||||
* @param pattern1 sync pattern for the burst currently aligned as timeslot 1
|
||||
* @param pattern2 sync pattern for the burst currently aligned as timeslot 2
|
||||
*/
|
||||
public void update(CACH cach1, CACH cach2, DMRSyncPattern pattern1, DMRSyncPattern pattern2)
|
||||
{
|
||||
if(cach1 != null && cach1.isValid())
|
||||
{
|
||||
if(cach1.isTimeslot0())
|
||||
{
|
||||
mBurst1Ts1++;
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurst1Ts2++;
|
||||
}
|
||||
}
|
||||
|
||||
if(cach2 != null && cach2.isValid())
|
||||
{
|
||||
if(cach2.isTimeslot0())
|
||||
{
|
||||
mBurst2Ts1++;
|
||||
}
|
||||
else
|
||||
{
|
||||
mBurst2Ts2++;
|
||||
}
|
||||
}
|
||||
|
||||
if(pattern1.isDirectMode())
|
||||
{
|
||||
switch((pattern1))
|
||||
{
|
||||
case DIRECT_MODE_DATA_TIMESLOT_1:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_1:
|
||||
mBurst1Ts1++;
|
||||
break;
|
||||
case DIRECT_MODE_DATA_TIMESLOT_2:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_2:
|
||||
mBurst1Ts2++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(pattern2.isDirectMode())
|
||||
{
|
||||
switch((pattern2))
|
||||
{
|
||||
case DIRECT_MODE_DATA_TIMESLOT_1:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_1:
|
||||
mBurst2Ts1++;
|
||||
break;
|
||||
case DIRECT_MODE_DATA_TIMESLOT_2:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_2:
|
||||
mBurst2Ts2++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tracker values.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mBurst1Ts1 = 0;
|
||||
mBurst1Ts2 = 0;
|
||||
mBurst2Ts1 = 0;
|
||||
mBurst2Ts2 = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("BURST 1: ").append(mBurst1Ts1).append("/").append(mBurst1Ts2);
|
||||
sb.append(" BURST 2:").append(mBurst2Ts1).append("/").append(mBurst2Ts2);
|
||||
if(hasSufficientData())
|
||||
{
|
||||
if(isAligned())
|
||||
{
|
||||
sb.append(" **ALIGNED**");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" >>MIS-ALIGNED<<");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -33,13 +33,14 @@ import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.IMessageProvider;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRToneIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.PiHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedEncryptionParameters;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -98,7 +99,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes DMR AMBE audio frame
|
||||
* Processes DMR AMBE audio frames and signalling messages.
|
||||
*/
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
@@ -108,49 +109,69 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
if(!mEncryptedCallStateEstablished)
|
||||
{
|
||||
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
|
||||
if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
|
||||
if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirect())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = false;
|
||||
}
|
||||
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
|
||||
//in the EMB field.
|
||||
else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
|
||||
else if(message instanceof VoiceEMBMessage voice)
|
||||
{
|
||||
if(voice.hasEmbeddedParameters() &&
|
||||
voice.getEmbeddedParameters().getShortBurst() instanceof EmbeddedEncryptionParameters)
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = true;
|
||||
}
|
||||
else if(voice.getEMB().isValid())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = voice.getEMB().isEncrypted();
|
||||
}
|
||||
}
|
||||
else if(message instanceof VoiceHeader vh && vh.getLCMessage() instanceof AbstractVoiceChannelUser vcu &&
|
||||
vcu.isValid())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = voiceMessage.getEMB().isEncrypted();
|
||||
mEncryptedCall = vcu.getServiceOptions().isEncrypted();
|
||||
}
|
||||
else if(message instanceof VoiceHeader voiceHeader)
|
||||
else if(message instanceof PiHeader pi && pi.getLCMessage() instanceof EncryptionParameters ep &&
|
||||
ep.isValid())
|
||||
{
|
||||
LCMessage lc = voiceHeader.getLCMessage();
|
||||
|
||||
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
|
||||
{
|
||||
if(flc.isValid())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = flc.isEncrypted();
|
||||
}
|
||||
}
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = true;
|
||||
}
|
||||
//Note: the DMRMessageProcessor extracts Full Link Control messages from Voice Frames B-C and sends them
|
||||
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of
|
||||
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
|
||||
else if(message instanceof AbstractVoiceChannelUser avcu)
|
||||
else if(message instanceof AbstractVoiceChannelUser avcu && avcu.isValid())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = avcu.getServiceOptions().isEncrypted();
|
||||
}
|
||||
|
||||
if(mEncryptedCall)
|
||||
{
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
}
|
||||
|
||||
//Queue or process audio frames. Note: audio frames are held/queued until encrypted state is established
|
||||
//before any audio is generated for the audio segment.
|
||||
if(message instanceof VoiceMessage voiceMessage)
|
||||
{
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
if(mEncryptedCallStateEstablished && mEncryptedCall)
|
||||
{
|
||||
processAudio(frame, message.getTimestamp());
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
processAudio(frame, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(message instanceof Terminator)
|
||||
@@ -168,23 +189,30 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
{
|
||||
if(mEncryptedCallStateEstablished)
|
||||
{
|
||||
//Process any ambe frames that were queued awaiting encryption state determination
|
||||
if(!mQueuedAmbeFrames.isEmpty())
|
||||
if(mEncryptedCall)
|
||||
{
|
||||
if(!mEncryptedCall)
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Process any ambe frames that were queued awaiting encryption state determination
|
||||
if(!mQueuedAmbeFrames.isEmpty())
|
||||
{
|
||||
for(byte[] queuedFrame: mQueuedAmbeFrames)
|
||||
if(!mEncryptedCall)
|
||||
{
|
||||
produceAudio(queuedFrame, timestamp);
|
||||
for(byte[] queuedFrame: mQueuedAmbeFrames)
|
||||
{
|
||||
produceAudio(queuedFrame, timestamp);
|
||||
}
|
||||
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
|
||||
mQueuedAmbeFrames.clear();
|
||||
produceAudio(frame, timestamp);
|
||||
}
|
||||
|
||||
produceAudio(frame, timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.IHamming;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) base implementation for BPTC codes using simple even parity column checks.
|
||||
*/
|
||||
public abstract class BPTCBase
|
||||
{
|
||||
public static int ERRORS_NOT_CORRECTED = -2;
|
||||
private IHamming mHamming;
|
||||
private int mColumnCount;
|
||||
private int mRowCount;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param hamming to provide row checking using the correct Hamming error detection and correction algorithm.
|
||||
* @param column count
|
||||
* @param row count
|
||||
*/
|
||||
public BPTCBase(IHamming hamming, int columnCount, int rowCount)
|
||||
{
|
||||
mHamming = hamming;
|
||||
mColumnCount = columnCount;
|
||||
mRowCount = rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction.
|
||||
* @param message to correct
|
||||
* @return true if the message was corrected or false if there are uncorrectable errors.
|
||||
*/
|
||||
public boolean correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
BinaryMessage columns = getColumnErrors(message);
|
||||
BinaryMessage rows = getRowErrors(message);
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
List<Integer> correctedIndexes = new ArrayList<>();
|
||||
|
||||
//1: fix non-shadowed single bit errors in each row
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
int index = correct1BitErrors(row, message, rows);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
correctedIndexes.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Refresh the column errors
|
||||
columns = getColumnErrors(message);
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
message.incrementCorrectedBitCount(correctedIndexes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
//2: fix non-shadowed multi bit errors (up to 2) in each row
|
||||
if(columns.cardinality() <= 2)
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
List<Integer> solution = correctMultiBitErrors(row, message, columns, rows);
|
||||
|
||||
if(solution.size() > 0)
|
||||
{
|
||||
correctedIndexes.addAll(solution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
message.incrementCorrectedBitCount(correctedIndexes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
message.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the column passes an even parity check.
|
||||
* @param column to test
|
||||
* @return true if the parity check passes
|
||||
*/
|
||||
public boolean isColumnCorrect(int column, BinaryMessage message)
|
||||
{
|
||||
boolean correct = true;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
correct ^= message.get(row * mColumnCount + column);
|
||||
}
|
||||
|
||||
return correct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the column from the bit index/offset
|
||||
* @param index for a bit
|
||||
* @return column that contains the index
|
||||
*/
|
||||
public int getColumn(int index)
|
||||
{
|
||||
return index % mColumnCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for a bit that exists at the row and column.
|
||||
* @param column for the bit.
|
||||
* @param row for the bit
|
||||
* @return index for the bit.
|
||||
*/
|
||||
public int getIndex(int column, int row)
|
||||
{
|
||||
return row * mColumnCount + column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the row for bit errors.
|
||||
* @param row to check
|
||||
* @param message containing bits to check
|
||||
* @return true if the row passes the Hamming check or false if not.
|
||||
*/
|
||||
private boolean isRowCorrect(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return mHamming.getErrorIndex(message, row * mColumnCount) == IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the bit error in the message
|
||||
* @param row to check
|
||||
* @param message with bits
|
||||
* @return error index in the range 0-195, or -1 if the message row has no bit errors.
|
||||
*/
|
||||
private int getRowErrorIndex(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return mHamming.getErrorIndex(message, row * mColumnCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bit map indicating the row errors.
|
||||
* @param message containing rows.
|
||||
* @return row error map
|
||||
*/
|
||||
public BinaryMessage getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
BinaryMessage rows = new BinaryMessage(mRowCount);
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
rows.set(row, !isRowCorrect(row, message));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bit map indicating the column errors.
|
||||
* @param message containing rows.
|
||||
* @return column error map
|
||||
*/
|
||||
public BinaryMessage getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
BinaryMessage columns = new BinaryMessage(mColumnCount);
|
||||
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
columns.set(column, !isColumnCorrect(column, message));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects single bit errors in each row using only the Hamming error index.
|
||||
* @param row to correct
|
||||
* @param message with bits
|
||||
* @param rows error flag set
|
||||
* @return bit index that was corrected or -2 if the error was not corrected
|
||||
*/
|
||||
public int correct1BitErrors(int row, CorrectedBinaryMessage message, BinaryMessage rows)
|
||||
{
|
||||
int index = getRowErrorIndex(row, message);
|
||||
|
||||
if(index >= 0 && index < IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
rows.clear(row);
|
||||
return index;
|
||||
}
|
||||
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return ERRORS_NOT_CORRECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects multiple bit errors in a single row when all other rows are corrected first, by flipping the
|
||||
* bits in the row that show errors in the columns and testing the solution.
|
||||
* @param row to correct
|
||||
* @param message with bits
|
||||
* @param columns bit error matrix
|
||||
* @param rows bit error matrix
|
||||
* @return solution or an empty list if no solution.
|
||||
*/
|
||||
public List<Integer> correctMultiBitErrors(int row, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows)
|
||||
{
|
||||
//Test flipping bits in the row everywhere that we have a column error
|
||||
List<Integer> testFlips = new ArrayList<>();
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < mColumnCount;
|
||||
column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
int toFlip = getIndex(column, row);
|
||||
message.flip(toFlip);
|
||||
testFlips.add(toFlip);
|
||||
}
|
||||
|
||||
if(!isRowCorrect(row, message))
|
||||
{
|
||||
int index = getRowErrorIndex(row, message);
|
||||
|
||||
if(index >= 0 && index < IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(testFlips.contains(index))
|
||||
{
|
||||
testFlips.removeIf(value -> value == index);
|
||||
}
|
||||
else
|
||||
{
|
||||
testFlips.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
//Check the columns also
|
||||
|
||||
boolean columnsCorrect = true;
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
columnsCorrect &= isColumnCorrect(getColumn(flip), message);
|
||||
}
|
||||
|
||||
if(columnsCorrect)
|
||||
{
|
||||
rows.clear(row);
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
columns.clear(getColumn(flip));
|
||||
}
|
||||
|
||||
return testFlips;
|
||||
}
|
||||
}
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
message.flip(flip);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void logErrorMap(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
int offset = row * mColumnCount;
|
||||
sb.append("Row ").append(row).append(": ");
|
||||
sb.append(message.getSubMessage(offset, offset + mColumnCount));
|
||||
sb.append(" ").append(offset).append(":").append(offset + mColumnCount).append("\n");
|
||||
}
|
||||
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
public void logDiagnostic(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
int offset = row * mColumnCount;
|
||||
sb.append(" Row ").append(row).append(": ");
|
||||
sb.append(message.getSubMessage(offset, offset + mColumnCount));
|
||||
int index = getRowErrorIndex(row, message);
|
||||
if(index != IHamming.NO_ERRORS)
|
||||
{
|
||||
sb.append(" < Index:").append(index);
|
||||
|
||||
if(index == IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
sb.append(" (2+ bit errors)");
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append("Errors: ");
|
||||
|
||||
BinaryMessage columnErrors = getColumnErrors(message);
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
if(columnErrors.get(column))
|
||||
{
|
||||
sb.append("^");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(".");
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Diagnostic:\n" + sb + "\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming16;
|
||||
|
||||
/**
|
||||
* Block product turbo code (BPTC) for decoding Full Link Control messages reassembled from the 32-bit payload fragments
|
||||
* in each of the Voice B-E messages.
|
||||
*/
|
||||
public class BPTC_128_77 extends BPTCBase
|
||||
{
|
||||
public BPTC_128_77()
|
||||
{
|
||||
super(new Hamming16(), 16, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction and extracts the payload from the BPTC encoded message.
|
||||
* @param message with BPTC encoding.
|
||||
* @return decoded message.
|
||||
*/
|
||||
public CorrectedBinaryMessage extract(BinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||
correct(deinterleaved);
|
||||
|
||||
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(77);
|
||||
int pointer = 0;
|
||||
|
||||
//Extract the message. Row 1 and 2 are 11 bits. Rows 3-7 are 10 bits.
|
||||
for(int row = 0; row < 2; row++)
|
||||
{
|
||||
for(int column = 0; column < 11; column++)
|
||||
{
|
||||
extractedMessage.set(pointer++, deinterleaved.get(row * 16 + column));
|
||||
}
|
||||
}
|
||||
|
||||
for(int row = 2; row < 7; row++)
|
||||
{
|
||||
for(int column = 0; column < 10; column++)
|
||||
{
|
||||
extractedMessage.set(pointer++, deinterleaved.get(row * 16 + column));
|
||||
}
|
||||
|
||||
//Transfer the checksum parity bit from this row
|
||||
extractedMessage.set(70 + row, deinterleaved.get(row * 16 + 10));
|
||||
}
|
||||
|
||||
extractedMessage.setCorrectedBitCount(deinterleaved.getCorrectedBitCount());
|
||||
return extractedMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deinterleaves the transmitted message.
|
||||
* @param interleaved message
|
||||
* @return deinterleaved message.
|
||||
*/
|
||||
public CorrectedBinaryMessage deinterleave(BinaryMessage interleaved)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(128);
|
||||
int index;
|
||||
deinterleaved.set(127, interleaved.get(127));
|
||||
for(int i = 0; i < 127; i++)
|
||||
{
|
||||
index = (i * 8) % 127;
|
||||
deinterleaved.set(i, interleaved.get(index));
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
BPTC_128_77 bptc = new BPTC_128_77();
|
||||
String deinterleavedRaw = "00000000000000000000000000000000001000000010010101011001001000010000000000000000000000000110101000010110111100010000111110011111";
|
||||
|
||||
String deinterleavedReference = "00000000000000000000000000000000000000000010011100011001001000110000000000000000000000000110101000010110111100010000111110011111"; //No errors
|
||||
CorrectedBinaryMessage deinterleavedReferenceMessage = new CorrectedBinaryMessage(BinaryMessage.load(deinterleavedReference));
|
||||
|
||||
String interleaved = "00000000000000000000000000010010000100010000001000000010000100010000001100000110001101100000001100000101001000010010010100110011"; //Under test
|
||||
CorrectedBinaryMessage interleavedMessage = new CorrectedBinaryMessage(BinaryMessage.load(interleaved));
|
||||
// CorrectedBinaryMessage deinterleaved = deinterleave(interleavedMessage);
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BinaryMessage.load(deinterleavedRaw));
|
||||
|
||||
|
||||
String deinterleavedUncorrected = deinterleaved.toString();
|
||||
bptc.logDiagnostic(deinterleaved);
|
||||
bptc.correct(deinterleaved);
|
||||
|
||||
System.out.println("DEINTERLEAVED: " + deinterleavedUncorrected);
|
||||
System.out.println(" CORRECTED: " + deinterleaved);
|
||||
System.out.println(" REFERENCE: " + deinterleavedReferenceMessage);
|
||||
|
||||
deinterleaved.xor(deinterleavedReferenceMessage);
|
||||
System.out.println(" DELTA: " + deinterleaved);
|
||||
|
||||
if(deinterleaved.cardinality() > 0)
|
||||
{
|
||||
System.out.println("--------------------------------");
|
||||
System.out.println("Residual Error Map:");
|
||||
bptc.logErrorMap(deinterleaved);
|
||||
System.out.println("--------------------------------");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.println("--------------------------------");
|
||||
System.out.println("Message Was Fully Corrected");
|
||||
System.out.println("--------------------------------");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,10 +17,11 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field.
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.BitSetFullException;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) 196/96 Support
|
||||
*/
|
||||
public class BPTC_196_96
|
||||
{
|
||||
public static final int BPTC_LENGTH = 196;
|
||||
public static final int EXTRACTED_LENGTH = 96; //However, we set the 3x reserved bits in 96, 97, and 98 making the length 99
|
||||
public static final int MAX_ORIGINAL_INDEX = 136;
|
||||
public static final int COLUMN_COUNT = 15;
|
||||
public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
|
||||
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;
|
||||
|
||||
/**
|
||||
* DMR De-interleaving indices
|
||||
*/
|
||||
public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182,
|
||||
167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3,
|
||||
184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20,
|
||||
5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37,
|
||||
22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54,
|
||||
39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71,
|
||||
56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103,
|
||||
88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120,
|
||||
105, 90, 75, 60, 45, 30, 15};
|
||||
|
||||
/**
|
||||
* DMR Column Indices
|
||||
*/
|
||||
public static int[][] COLUMN_INDEXES = new int[][]{
|
||||
{1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181},
|
||||
{2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182},
|
||||
{3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183},
|
||||
{4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184},
|
||||
{5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185},
|
||||
{6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186},
|
||||
{7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187},
|
||||
{8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188},
|
||||
{9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189},
|
||||
{10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190},
|
||||
{11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191},
|
||||
{12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192},
|
||||
{13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193},
|
||||
{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}};
|
||||
|
||||
/**
|
||||
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
|
||||
* protected message
|
||||
* @param original uncorrected BPTC(196,96) message
|
||||
* @return error corrected and extracted 96-bit message
|
||||
*/
|
||||
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
|
||||
{
|
||||
//De-Interleave
|
||||
CorrectedBinaryMessage message = deinterleave(original);
|
||||
|
||||
//Perform error detection and correction
|
||||
boolean pursueShadows = true;
|
||||
correct(message, pursueShadows);
|
||||
|
||||
//Extract the 96-bit message from the corrected 196 bits
|
||||
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
|
||||
|
||||
//Transfer the corrected bit count
|
||||
extracted.setCorrectedBitCount(message.getCorrectedBitCount());
|
||||
|
||||
int index = MESSAGE_START_INDEX;
|
||||
|
||||
while(index < MAX_ORIGINAL_INDEX)
|
||||
{
|
||||
if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT)
|
||||
{
|
||||
try
|
||||
{
|
||||
extracted.add(message.get(index));
|
||||
}
|
||||
catch(BitSetFullException bsfe)
|
||||
{
|
||||
//Should never get to here
|
||||
bsfe.printStackTrace();
|
||||
return extracted;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
index += CHECKSUM_COLUMN_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
//Transfer bits R2, R1, and R0 to the end - RAS bits
|
||||
extracted.set(96, message.get(0));
|
||||
extracted.set(97, message.get(1));
|
||||
extracted.set(98, message.get(2));
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
|
||||
* @param message that contains interleaved (transmitted) message
|
||||
* @return de-interleaved message
|
||||
*/
|
||||
private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH);
|
||||
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
|
||||
{
|
||||
deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x]));
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the row level Hamming(15,11,3) protected words
|
||||
* @param message containing protected words in rows
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int index = -1;
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
//The first bit of the message is a pad/reserved bit and is ignored.
|
||||
offset = (row * 15) + 1;
|
||||
index = Hamming15.getErrorIndex(message, offset);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
rowErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Note: row errors should already be in order and don't need sorted
|
||||
return rowErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the column level Hamming(13,9,3) protected words
|
||||
* @param message containing protected words in columns
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> columnErrors = new ArrayList<>();
|
||||
|
||||
int index = -1;
|
||||
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
columnErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(columnErrors);
|
||||
return columnErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of the values that intersect between list 1 and list 2.
|
||||
* @param list1 to compare
|
||||
* @param list2 to compare
|
||||
* @return intersection
|
||||
*/
|
||||
private static List<Integer> intersection(List<Integer> list1, List<Integer> list2)
|
||||
{
|
||||
List<Integer> intersection = new ArrayList<>();
|
||||
|
||||
for(Integer i : list1)
|
||||
{
|
||||
if(list2.contains(i))
|
||||
{
|
||||
intersection.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
private static void logDiagnostic(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
int offset = (row * 15) + 1;
|
||||
sb.append("Row ");
|
||||
if(row < 10)
|
||||
{
|
||||
sb.append(" ").append(row);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(row);
|
||||
}
|
||||
sb.append(": ");
|
||||
sb.append(message.getSubMessage(offset, offset + 16));
|
||||
int index = getRowErrorIndex(row, message);
|
||||
sb.append(" Index:").append(index == IHamming.NO_ERRORS ? "-": index);
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append("Column Error Indices: ");
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
int index = getColumnErrorIndex(column, message);
|
||||
String label = index == IHamming.NO_ERRORS ? "-" : (index == 1000 ? "(bad)" : String.valueOf(index));
|
||||
sb.append(column).append(":").append(label).append(" ");
|
||||
}
|
||||
|
||||
System.out.println("Diagnostic:\n" + sb + "\n");
|
||||
}
|
||||
|
||||
private static void logErrorMap(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
int offset = (row * 15) + 1;
|
||||
sb.append("Row ");
|
||||
if(row < 10)
|
||||
{
|
||||
sb.append(" ").append(row);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(row);
|
||||
}
|
||||
sb.append(": ");
|
||||
sb.append(message.getSubMessage(offset, offset + 15));
|
||||
|
||||
sb.append(" ").append(offset).append(":").append(offset + 14).append(" =");
|
||||
|
||||
for(int index = offset; index < offset + 15; index++)
|
||||
{
|
||||
if(message.get(index))
|
||||
{
|
||||
sb.append(" " + index);
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
System.out.println("Error Map:\n" + sb + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the indices that make up the intersections of the row and column errors.
|
||||
* @param rows bitmap indicating rows with errors
|
||||
* @param columns bitmap indicating columns with errors
|
||||
* @return set of intersection indices.
|
||||
*/
|
||||
public static List<Integer> getIntersectionIndices(BinaryMessage columns, BinaryMessage rows)
|
||||
{
|
||||
List<Integer> intersections = new ArrayList<>();
|
||||
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
intersections.add(getIndex(column, row));
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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)
|
||||
{
|
||||
List<Integer> columnErrors = getColumnErrors(message);
|
||||
List<Integer> rowErrors = getRowErrors(message);
|
||||
|
||||
if(columnErrors.isEmpty() && rowErrors.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Fix the easy bits first.
|
||||
List<Integer> flippedBits = new ArrayList<>();
|
||||
List<Integer> intersection = intersection(columnErrors, rowErrors);
|
||||
|
||||
if(!intersection.isEmpty())
|
||||
{
|
||||
for(Integer i : intersection)
|
||||
{
|
||||
message.flip(i);
|
||||
flippedBits.add(i);
|
||||
}
|
||||
|
||||
message.incrementCorrectedBitCount(intersection.size());
|
||||
}
|
||||
|
||||
//Set of flags for columns with errors
|
||||
BinaryMessage columns = new BinaryMessage(15);
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
columns.set(column, !isColumnCorrect(column, message));
|
||||
}
|
||||
|
||||
//Set of flags for rows with errors
|
||||
BinaryMessage rows = new BinaryMessage(13);
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
rows.set(row, !isRowCorrect(row, message));
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
return true; //No (more) errors
|
||||
}
|
||||
|
||||
//1: fix up to 2-bit errors per column or row without pursuing multi-column or multi-row paths
|
||||
boolean pursue = false;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
pursue = true;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Detect double/triple row/column bit errors that are shadowing each other and preventing the
|
||||
// Hamming codes from giving accurate bit error locations. Flip all of their intersections and
|
||||
// then attempt to correct the message again. We use a flag 'pursueShadows' to ensure that we
|
||||
// only chase the shadow once, not recursively, since this parent method can be called recursively.
|
||||
if(pursueShadows && ((columns.cardinality() == 2 || columns.cardinality() == 3) &&
|
||||
(rows.cardinality() == 2 || rows.cardinality() == 3)))
|
||||
{
|
||||
List<Integer> intersectionIndices = getIntersectionIndices(columns, rows);
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
|
||||
//Set pursue shadows to false so we don't recursively chase shadows
|
||||
pursueShadows = false;
|
||||
|
||||
if(correct(message, pursueShadows))
|
||||
{
|
||||
columns.clear();
|
||||
rows.clear();
|
||||
int correctedBits = intersectionIndices.size() - message.getCorrectedBitCount();
|
||||
message.incrementCorrectedBitCount(correctedBits);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
Collections.sort(flippedBits);
|
||||
message.incrementCorrectedBitCount(flippedBits.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
message.setCorrectedBitCount(BPTCBase.ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive method ...
|
||||
* @param column
|
||||
* @param message
|
||||
* @param columns
|
||||
* @param rows
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> correctColumn(int column, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
{
|
||||
//Don't let the recursive call exceed threshold
|
||||
if(depth > MAXIMUM_RECURSION_TURBO_DEPTH)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = getColumnErrorIndex(column, message);
|
||||
int row = getRow(index);
|
||||
|
||||
//If the candidate index is in range and the row for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && rows.get(row) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isColumnCorrect(column, message))
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the row is now correct, stop here.
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the row. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
//If our candidate index is part of the discovered solution, fail, otherwise return it as part of
|
||||
// the solution.
|
||||
if(solution.size() > 0)
|
||||
{
|
||||
columns.clear(column);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional column flips
|
||||
{
|
||||
for(int column2 = columns.nextSetBit(column + 1); column2 >= 0 && column2 < 15; column2 = columns.nextSetBit(column2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctColumn(column2, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(solution2.size() > 0)
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static List<Integer> correctRow(int row, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
{
|
||||
//Don't let the recursive call exceed threshold
|
||||
if(depth > MAXIMUM_RECURSION_TURBO_DEPTH)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = getRowErrorIndex(row, message);
|
||||
int column = getColumn(index);
|
||||
|
||||
//If the candidate index is in range and the column for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && columns.get(column) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the column is now correct, stop here and return the path as the solution
|
||||
if(isColumnCorrect(column, message))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the column. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
//If our candidate index is part of the discovered solution, fail, otherwise return it as part of
|
||||
// the solution.
|
||||
if(solution.size() > 0)
|
||||
{
|
||||
rows.clear(row);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional row flips
|
||||
{
|
||||
for(int row2 = rows.nextSetBit(row + 1); row2 >= 0 && row2 < 13; row2 = rows.nextSetBit(row2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctRow(row2, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(solution2.size() > 0 && !solution2.contains(index))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static int getIndex(int column, int row)
|
||||
{
|
||||
return row * 15 + column + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the row for bit errors.
|
||||
* @param row to check
|
||||
* @param message containing bits to check
|
||||
* @return true if the row passes the Hamming check or false if not.
|
||||
*/
|
||||
private static boolean isRowCorrect(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming15.getSyndrome(message, (row * 15) + 1) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the bit error in the message
|
||||
* @param row to check
|
||||
* @param message with bits
|
||||
* @return error index in the range 0-195, or -1 if the message row has no bit errors.
|
||||
*/
|
||||
private static int getRowErrorIndex(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming15.getErrorIndex(message, (row * 15) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the column for bit errors.
|
||||
* @param column to check
|
||||
* @param message containing bits to check
|
||||
* @return true if the column passes the Hamming check or false if not.
|
||||
*/
|
||||
private static boolean isColumnCorrect(int column, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming13.getSyndrome(message, COLUMN_INDEXES[column]) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the bit error in the message.
|
||||
* @param column to check
|
||||
* @param message with bits.
|
||||
* @return error index in the range 0-195, or -1 if message has no bit errors, or 1000 if message has more than 1 bit error.
|
||||
*/
|
||||
private static int getColumnErrorIndex(int column, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
}
|
||||
|
||||
public static int getColumn(int index)
|
||||
{
|
||||
return (index - 1) % 15;
|
||||
}
|
||||
|
||||
public static int getRow(int index)
|
||||
{
|
||||
return (index - 1) / 15;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String raw = "0000101111101000000100001010100001010000001000000011100000000000100000000000001000000000001000000000000010000000001111001010000100001000000001100000100001000111110101011111011111101001111011100010";
|
||||
String ref = "0000101111100000000100001100100001010000001000000000000000000000000000000000000000000000000000000000000000000000001111001010000100001000000001110000100001011111110101011011011111101011111011100000";
|
||||
CorrectedBinaryMessage rawMessage = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
CorrectedBinaryMessage refMessage = new CorrectedBinaryMessage(BinaryMessage.load(ref));
|
||||
|
||||
// correct2(refMessage);
|
||||
|
||||
boolean pursueShadows = true;
|
||||
correct(rawMessage, pursueShadows);
|
||||
System.out.println("RAW: " + raw);
|
||||
System.out.println("COR: " + rawMessage);
|
||||
System.out.println("REF: " + ref);
|
||||
|
||||
rawMessage.xor(refMessage);
|
||||
System.out.println("RES: " + rawMessage);
|
||||
|
||||
if(rawMessage.cardinality() > 0)
|
||||
{
|
||||
System.out.println("Residual Error Map:");
|
||||
logErrorMap(rawMessage);
|
||||
}
|
||||
|
||||
rawMessage = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
rawMessage.xor(refMessage);
|
||||
System.out.println("\n\nXOR: " + rawMessage);
|
||||
logErrorMap(rawMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming17;
|
||||
|
||||
/**
|
||||
* Block product turbo code (BPTC) for decoding Short Link Control reassembled from CACH fragments.
|
||||
*/
|
||||
public class BPTC_68_36 extends BPTCBase
|
||||
{
|
||||
public BPTC_68_36()
|
||||
{
|
||||
super(new Hamming17(), 17, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction and extracts the payload from the BPTC encoded message.
|
||||
* @param message with BPTC encoding.
|
||||
* @return decoded message.
|
||||
*/
|
||||
public CorrectedBinaryMessage extract(BinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||
correct(deinterleaved);
|
||||
|
||||
//Extract the message
|
||||
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(36);
|
||||
|
||||
for(int row = 0; row < 3; row++)
|
||||
{
|
||||
for(int column = 0; column < 12; column++)
|
||||
{
|
||||
extractedMessage.set(row * 12 + column, deinterleaved.get(row * 17 + column));
|
||||
}
|
||||
}
|
||||
|
||||
extractedMessage.setCorrectedBitCount(deinterleaved.getCorrectedBitCount());
|
||||
return extractedMessage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deinterleaves the transmitted message.
|
||||
* @param interleaved message
|
||||
* @return deinterleaved message.
|
||||
*/
|
||||
public CorrectedBinaryMessage deinterleave(BinaryMessage interleaved)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(68);
|
||||
|
||||
for(int index = 0; index < 67; index++)
|
||||
{
|
||||
if(interleaved.get(index))
|
||||
{
|
||||
deinterleaved.set((index * 17 % 67));
|
||||
}
|
||||
}
|
||||
|
||||
if(interleaved.get(67))
|
||||
{
|
||||
deinterleaved.set(67);
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
BPTC_68_36 bptc = new BPTC_68_36();
|
||||
|
||||
String ref = "10101001100001000000001101101111000000110011001110011111101011001100"; //No errors
|
||||
String raw = "10101001100001000000001101101111000000110011001110011111101011001100"; //Under test
|
||||
CorrectedBinaryMessage rawMessage = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
CorrectedBinaryMessage refMessage = new CorrectedBinaryMessage(BinaryMessage.load(ref));
|
||||
|
||||
bptc.logDiagnostic(rawMessage);
|
||||
|
||||
bptc.correct(rawMessage);
|
||||
|
||||
System.out.println("RAW: " + raw);
|
||||
System.out.println("COR: " + rawMessage);
|
||||
System.out.println("REF: " + ref);
|
||||
|
||||
rawMessage.xor(refMessage);
|
||||
System.out.println("RES: " + rawMessage);
|
||||
|
||||
if(rawMessage.cardinality() > 0)
|
||||
{
|
||||
System.out.println("--------------------------------");
|
||||
System.out.println("Residual Error Map:");
|
||||
bptc.logErrorMap(rawMessage);
|
||||
System.out.println("--------------------------------");
|
||||
}
|
||||
|
||||
System.out.println("\n\n*********** TRUTH DATA ********************");
|
||||
rawMessage = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
rawMessage.xor(refMessage);
|
||||
System.out.println("XOR: " + rawMessage);
|
||||
bptc.logErrorMap(rawMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,49 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.message;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.bits.IntField;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Common Announcement Channel is a 24-bit sequence that precedes an outbound DMR frame transmitted by a repeater.
|
||||
*/
|
||||
public class CACH
|
||||
{
|
||||
//Generated by method: createHamming7_4BitErrorMap(). The value of the uncorrected 7-bit CACH is used as an index
|
||||
//to lookup the bit error position where:
|
||||
// 0-6: bit error index location where bits are ordered as transmitted: 0, 1 ..., 6
|
||||
// -1: valid/correct message
|
||||
// -2: invalid message with 2 or more bit errors
|
||||
public static final int[] BIT_ERROR_INDEXES = new int[]{-2, -2, -2, -2, -2, -2, -2, -2, -2, 5, 6, -1, 1, 2, 0, 4,
|
||||
-2, 1, 4, 0, 5, 3, -1, 6, 0, 4, 1, 2, 6, -1, 3, 5, -2, -2, -2, -2, -2, -2, -2, -2, 4, 0, 2, 1, -1, 6, 5, -2,
|
||||
6, -1, 3, 5, 0, 4, 1, -2, 5, 3, -1, 6, 2, 1, 4, 0, -2, -2, -2, -2, -2, -2, -2, -2, 2, 1, 4, 0, 5, -2, -1, 6,
|
||||
3, 5, 6, -1, 1, -2, 0, 4, -1, 6, 5, 3, 4, 0, 2, 1, -2, -2, -2, -2, -2, -2, -2, -2, 6, -1, -2, 5, 0, 4, 1, 2,
|
||||
4, 0, -2, 1, -1, 6, 5, 3, 1, 2, 0, 4, 3, 5, 6, -1};
|
||||
|
||||
private static final IntField VALUE_FIELD = IntField.range(0, 6);
|
||||
|
||||
private static final int CACH_MESSAGE_LENGTH = 24;
|
||||
private static final int[] INTERLEAVE_MATRIX = new int[]{0, 4, 8, 12, 14, 18, 22, 1, 2, 3, 5, 6, 7, 9, 10, 11, 13,
|
||||
15, 16, 17, 19, 20, 21, 23};
|
||||
@@ -38,6 +73,23 @@ public class CACH
|
||||
mValid = valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("[CRC-ERROR] ");
|
||||
}
|
||||
sb.append(getInboundChannelAccessType().name()).append(" ");
|
||||
sb.append(getLCSS());
|
||||
sb.append(" TS:").append(getTimeslot());
|
||||
sb.append(" CRC:").append(getCrcChecksum(getMessage()));
|
||||
sb.append(" PAYLOAD:").append(getPayload().toHexString());
|
||||
sb.append(" MSG:").append(getMessage().toHexString()).append(" ");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Descrambled message bits for this cach
|
||||
*/
|
||||
@@ -64,18 +116,6 @@ public class CACH
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// sb.append(getMessage().toHexString()).append(" ");
|
||||
// sb.append(getInboundChannelAccessType().name()).append(" ");
|
||||
sb.append(getLCSS());
|
||||
// sb.append(" (").append(getTimeslot()).append("|").append(getCrcChecksum(getMessage())).append(")");
|
||||
// sb.append(" ").append(getPayload().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this cach passes the FEC error correction and indicates that the inbound channel Access Type,
|
||||
* Timeslot and LCSS information is valid.
|
||||
@@ -109,17 +149,17 @@ public class CACH
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the outbound timeslot for the next frame that follows this CACH, 0 or 1
|
||||
* Indicates the outbound timeslot for the next frame that follows this CACH, 1 or 2
|
||||
*/
|
||||
public int getTimeslot()
|
||||
{
|
||||
return mMessage.get(OUTBOUND_BURST_TIMESLOT) ? 1 : 0;
|
||||
return mMessage.get(OUTBOUND_BURST_TIMESLOT) ? 2 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the timeslot is 0
|
||||
*/
|
||||
public boolean isTimeslot0()
|
||||
public boolean isTimeslot1()
|
||||
{
|
||||
return !mMessage.get(OUTBOUND_BURST_TIMESLOT);
|
||||
}
|
||||
@@ -142,7 +182,6 @@ public class CACH
|
||||
{
|
||||
CorrectedBinaryMessage decodedMessage = new CorrectedBinaryMessage(CACH_MESSAGE_LENGTH);
|
||||
|
||||
|
||||
//Deinterleave the transmitted message to create the decoded message
|
||||
for(int x = 0; x < CACH_MESSAGE_LENGTH; x++)
|
||||
{
|
||||
@@ -152,14 +191,15 @@ public class CACH
|
||||
}
|
||||
}
|
||||
|
||||
int checksum = getCrcChecksum(decodedMessage);
|
||||
int codeword = decodedMessage.getInt(VALUE_FIELD);
|
||||
int bitError = BIT_ERROR_INDEXES[codeword];
|
||||
|
||||
// //debug - investigating corruption of CACH, SLC and EMB during encrypted Hytera/MotoTRBO calls.
|
||||
// BinaryMessage test = message.getSubMessage(0, CACH_MESSAGE_LENGTH);
|
||||
// System.out.println("--> CACH:" + test.toString() + " DE:" + decodedMessage.toString() + " CHK:" + checksum);
|
||||
|
||||
boolean valid = (checksum == 0);
|
||||
if(bitError >= 0)
|
||||
{
|
||||
decodedMessage.flip(bitError);
|
||||
}
|
||||
|
||||
boolean valid = bitError != -2;
|
||||
return new CACH(decodedMessage, valid);
|
||||
}
|
||||
|
||||
@@ -184,4 +224,101 @@ public class CACH
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
public static void createHamming7_4BitErrorMap()
|
||||
{
|
||||
IntField valueField = IntField.range(0, 6);
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
|
||||
//Link Control Start/Stop value 00 is not valid for CACH, so we'll mark these as invalid codewords (-2).
|
||||
int invalidLCSSMask = 0x18; //0011000b
|
||||
|
||||
BinaryMessage parity0 = new BinaryMessage(7);
|
||||
parity0.load(4, 3, 5);
|
||||
BinaryMessage parity1 = new BinaryMessage(7);
|
||||
parity1.load(4, 3, 7);
|
||||
BinaryMessage parity2 = new BinaryMessage(7);
|
||||
parity2.load(4, 3, 6);
|
||||
BinaryMessage parity3 = new BinaryMessage(7);
|
||||
parity3.load(4, 3, 3);
|
||||
|
||||
//There are 2^4 = 16 potential valid codewords
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
BinaryMessage bm = new BinaryMessage(7);
|
||||
bm.load(0, 4, x);
|
||||
|
||||
if(bm.get(0))
|
||||
{
|
||||
bm.xor(parity0);
|
||||
}
|
||||
if(bm.get(1))
|
||||
{
|
||||
bm.xor(parity1);
|
||||
}
|
||||
if(bm.get(2))
|
||||
{
|
||||
bm.xor(parity2);
|
||||
}
|
||||
if(bm.get(3))
|
||||
{
|
||||
bm.xor(parity3);
|
||||
}
|
||||
|
||||
int value = bm.getInt(valueField);
|
||||
|
||||
//Ignore any legal codeword that has a value of 00 in the LCSS field.
|
||||
if(((value & invalidLCSSMask) ^ invalidLCSSMask) == invalidLCSSMask)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//Use -1 to indicate no errors and we'll reserve -2 for invalid/non-correctable 2-bit errors.
|
||||
map.put(value, -1);
|
||||
|
||||
//Iteratively induce single bit errors and record them into the map
|
||||
for(int bitError = 0; bitError < 7; bitError++)
|
||||
{
|
||||
bm.flip(bitError);
|
||||
value = bm.getInt(valueField);
|
||||
|
||||
//Ignore any bit combination with a 00 in the LCSS field.
|
||||
if(((value & invalidLCSSMask) ^ invalidLCSSMask) != invalidLCSSMask)
|
||||
{
|
||||
map.put(value, bitError);
|
||||
}
|
||||
|
||||
bm.flip(bitError);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("public static final int[] BIT_ERROR_INDEXES = new int[]{");
|
||||
|
||||
for(int z = 0; z < 128; z++)
|
||||
{
|
||||
if(map.containsKey(z))
|
||||
{
|
||||
sb.append(map.get(z));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("-2");
|
||||
}
|
||||
|
||||
if(z != 127)
|
||||
{
|
||||
sb.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("};");
|
||||
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
createHamming7_4BitErrorMap();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ package io.github.dsheirer.module.decode.dmr.message;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
/**
|
||||
* Base DMR Burst Message class
|
||||
*/
|
||||
|
||||
@@ -20,63 +20,21 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.TimeslotMessage;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Base DMR Message
|
||||
*/
|
||||
public abstract class DMRMessage extends Message
|
||||
public abstract class DMRMessage extends TimeslotMessage
|
||||
{
|
||||
public static final int TIMESLOT_1 = 1;
|
||||
public static final int TIMESLOT_2 = 2;
|
||||
|
||||
private CorrectedBinaryMessage mCorrectedBinaryMessage;
|
||||
private boolean mValid = true;
|
||||
private int mTimeslot;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param timestamp for the message
|
||||
*/
|
||||
public DMRMessage(CorrectedBinaryMessage message, long timestamp, int timeslot)
|
||||
{
|
||||
super(timestamp);
|
||||
mCorrectedBinaryMessage = message;
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message bits
|
||||
*/
|
||||
public CorrectedBinaryMessage getMessage()
|
||||
{
|
||||
return mCorrectedBinaryMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this message is valid
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return mValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the valid flag for this message
|
||||
*/
|
||||
public void setValid(boolean valid)
|
||||
{
|
||||
mValid = valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeslot for this message
|
||||
* @return 0 or 1
|
||||
*/
|
||||
public int getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
super(message, timeslot, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,11 +20,14 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.message.EmptyTimeslotPlaceholderMessage;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DMRDataMessageFactory;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceAMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -40,39 +43,33 @@ public class DMRMessageFactory
|
||||
* @param timeslot for the burst
|
||||
* @return DMRMessage instance or null for UNKNOWN sync pattern
|
||||
*/
|
||||
public static DMRBurst create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach,
|
||||
public static IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach,
|
||||
long timestamp, int timeslot)
|
||||
{
|
||||
switch(syncPattern)
|
||||
{
|
||||
case BASE_STATION_VOICE:
|
||||
case MOBILE_STATION_VOICE:
|
||||
case BS_VOICE_FRAME_B:
|
||||
case BS_VOICE_FRAME_C:
|
||||
case BS_VOICE_FRAME_D:
|
||||
case BS_VOICE_FRAME_E:
|
||||
case BS_VOICE_FRAME_F:
|
||||
|
||||
case MS_VOICE_FRAME_B:
|
||||
case MS_VOICE_FRAME_C:
|
||||
case MS_VOICE_FRAME_D:
|
||||
case MS_VOICE_FRAME_E:
|
||||
case MS_VOICE_FRAME_F:
|
||||
case BASE_STATION_VOICE, MOBILE_STATION_VOICE:
|
||||
case BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F:
|
||||
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);
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_1:
|
||||
case DIRECT_VOICE_TIMESLOT_1:
|
||||
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 1);
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_2:
|
||||
case DIRECT_VOICE_TIMESLOT_2:
|
||||
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 2);
|
||||
case BASE_STATION_DATA:
|
||||
case MOBILE_STATION_DATA:
|
||||
return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot);
|
||||
case DIRECT_MODE_DATA_TIMESLOT_1:
|
||||
case DIRECT_DATA_TIMESLOT_1:
|
||||
return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1);
|
||||
case DIRECT_MODE_DATA_TIMESLOT_2:
|
||||
case DIRECT_DATA_TIMESLOT_2:
|
||||
return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2);
|
||||
case DIRECT_EMPTY_TIMESLOT:
|
||||
return new EmptyTimeslotPlaceholderMessage(timestamp, 288, Protocol.DMR, timeslot);
|
||||
case UNKNOWN:
|
||||
return null;
|
||||
case MOBILE_STATION_REVERSE_CHANNEL:
|
||||
case REVERSE_CHANNEL:
|
||||
case RESERVED:
|
||||
default:
|
||||
return new UnknownDMRMessage(syncPattern, binaryMessage, cach, timestamp, timeslot);
|
||||
@@ -95,14 +92,13 @@ public class DMRMessageFactory
|
||||
{
|
||||
case BASE_STATION_VOICE:
|
||||
case MOBILE_STATION_VOICE:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_1:
|
||||
case DIRECT_MODE_VOICE_TIMESLOT_2:
|
||||
case DIRECT_VOICE_TIMESLOT_1:
|
||||
case DIRECT_VOICE_TIMESLOT_2:
|
||||
return new VoiceAMessage(syncPattern, message, cach, timestamp, timeslot);
|
||||
case BS_VOICE_FRAME_B:
|
||||
case BS_VOICE_FRAME_C:
|
||||
case BS_VOICE_FRAME_D:
|
||||
case BS_VOICE_FRAME_E:
|
||||
case BS_VOICE_FRAME_F:
|
||||
case BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F:
|
||||
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:
|
||||
default:
|
||||
return new VoiceEMBMessage(syncPattern, message, cach, timestamp, timeslot);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ package io.github.dsheirer.module.decode.dmr.message;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,10 +21,9 @@ package io.github.dsheirer.module.decode.dmr.message.data;
|
||||
|
||||
import io.github.dsheirer.bits.BitSetFullException;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.BPTC_196_96;
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.edac.trellis.ViterbiDecoder_3_4_DMR;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.bptc.BPTC_196_96;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock1Rate;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock1_2Rate;
|
||||
@@ -52,6 +51,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.usb.USBData;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -79,33 +79,73 @@ public class DMRDataMessageFactory
|
||||
|
||||
if(slotType.isValid())
|
||||
{
|
||||
switch (slotType.getDataType())
|
||||
{
|
||||
case RATE_3_OF_4_DATA:
|
||||
return new DataBlock3_4Rate(pattern, getTrellisPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case RATE_1_DATA:
|
||||
return new DataBlock1Rate(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
|
||||
//Get the BPTC(196,96) protected payload. If the corrected bit count is -1, the payload/message is invalid
|
||||
CorrectedBinaryMessage payload = getPayload(message);
|
||||
|
||||
switch(slotType.getDataType())
|
||||
{
|
||||
case SLOT_IDLE:
|
||||
return new IDLEMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage idle = new IDLEMessage(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
idle.setValid(false);
|
||||
}
|
||||
return idle;
|
||||
case CSBK:
|
||||
return CSBKMessageFactory.create(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage csbk = CSBKMessageFactory.create(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
csbk.setValid(false);
|
||||
}
|
||||
return csbk;
|
||||
case USB_DATA:
|
||||
CorrectedBinaryMessage usbdPayload = getPayload(message);
|
||||
CorrectedBinaryMessage usbdPayload = payload;
|
||||
switch(USBData.getServiceType(usbdPayload))
|
||||
{
|
||||
case LIP_SHORT_LOCATION_REQUEST:
|
||||
default:
|
||||
return new USBData(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
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:
|
||||
return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
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:
|
||||
return new PiHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
pi.setValid(false);
|
||||
}
|
||||
return pi;
|
||||
case VOICE_HEADER:
|
||||
return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
voice.setValid(false);
|
||||
}
|
||||
return voice;
|
||||
case DATA_ENC_HEADER:
|
||||
case DATA_HEADER:
|
||||
CorrectedBinaryMessage payload = getPayload(message);
|
||||
int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC);
|
||||
boolean valid = crcBitCount < 2;
|
||||
boolean valid = crcBitCount < 2 && payload.getCorrectedBitCount() != -1;
|
||||
DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload);
|
||||
|
||||
switch(dpf)
|
||||
@@ -191,22 +231,44 @@ public class DMRDataMessageFactory
|
||||
return dh;
|
||||
}
|
||||
case TLC:
|
||||
return new Terminator(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
tlc.setValid(false);
|
||||
}
|
||||
return tlc;
|
||||
case RATE_1_OF_2_DATA:
|
||||
return new DataBlock1_2Rate(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case RATE_3_OF_4_DATA:
|
||||
return new DataBlock3_4Rate(pattern, getTrellisPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case RATE_1_DATA:
|
||||
return new DataBlock1Rate(pattern, message, cach, slotType, timestamp, timeslot);
|
||||
DataMessage data = new DataBlock1_2Rate(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
data.setValid(false);
|
||||
}
|
||||
return data;
|
||||
case MBC_BLOCK:
|
||||
return new MBCContinuationBlock(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage mbcBlock = new MBCContinuationBlock(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
mbcBlock.setValid(false);
|
||||
}
|
||||
return mbcBlock;
|
||||
case RESERVED_15:
|
||||
case UNKNOWN:
|
||||
return new UnknownDataMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
unk.setValid(false);
|
||||
}
|
||||
return unk;
|
||||
}
|
||||
}
|
||||
|
||||
return new UnknownDataMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
CorrectedBinaryMessage payload = getPayload(message);
|
||||
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
|
||||
if(payload.getCorrectedBitCount() < 0)
|
||||
{
|
||||
unk.setValid(false);
|
||||
}
|
||||
return unk;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Base DMR Data Message
|
||||
|
||||
@@ -21,11 +21,11 @@ package io.github.dsheirer.module.decode.dmr.message.data;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,8 +20,8 @@ package io.github.dsheirer.module.decode.dmr.message.data;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -54,6 +54,11 @@ public class IDLEMessage extends DataMessage
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC-ERROR]");
|
||||
}
|
||||
|
||||
sb.append(" IDLE");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ package io.github.dsheirer.module.decode.dmr.message.data;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.block;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.block;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Rate 1/1 Data Block
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.block;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Rate 1/2 BPTC 196 Data Block
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.block;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Rate 3/4 Trellis Coded Data Block
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.block;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Unknown DMR data block
|
||||
|
||||
@@ -21,12 +21,12 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Control Signalling Block (CSBK) Message
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.Hytera08Acknowledge;
|
||||
@@ -88,6 +87,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.mbc.UnknownMultiCSBK;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceKind;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
@@ -31,6 +30,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AcknowledgeType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Reason;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
@@ -31,6 +30,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AcknowledgeType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Reason;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AdjacentSiteInformation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
|
||||
/**
|
||||
* Hytera Neighbor/Adjacent Site Information
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
@@ -30,6 +29,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceFunction;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Version;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -30,6 +29,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DmrTier3Radio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
@@ -30,6 +29,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceFunction;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Version;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -30,7 +29,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -30,6 +29,7 @@ import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
@@ -34,6 +33,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -30,6 +29,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.site.SiteIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRSite;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPMessageType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user