#2162 APCO25 NID BCH decoder implementation. (#2163)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2025-02-27 08:09:46 -06:00
committed by GitHub
parent 4e367bb5fe
commit 8718d04ef5
6 changed files with 1513 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -415,8 +415,8 @@ public class BinaryMessage extends BitSet
boolean[] returnValue = null;
if(startIndex >= 0 &&
startIndex < endIndex &&
endIndex < mSize)
startIndex < endIndex &&
endIndex < mSize)
{
returnValue = new boolean[endIndex - startIndex + 1];
@@ -447,7 +447,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -676,7 +676,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -711,6 +711,28 @@ public class BinaryMessage extends BitSet
}
}
/**
* Sets the integer value to the field described by the argument.
* @param value to set
* @param intField describing the indices of the field.
*/
public void setInt(int value, IntField intField)
{
for(int x = 0; x < intField.width(); x++)
{
int mask = 1 << (intField.width() - x - 1);
if((value & mask) == mask)
{
set(intField.start() + x);
}
else
{
clear(intField.start() + x);
}
}
}
/**
* Returns the byte value represented by the bit array
*
@@ -724,7 +746,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -756,7 +778,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -884,7 +906,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -916,7 +938,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -1004,7 +1026,7 @@ public class BinaryMessage extends BitSet
else
{
throw new IllegalArgumentException("BitSetBuffer.getHex() "
+ "maximum array length is 63 bits");
+ "maximum array length is 63 bits");
}
}
@@ -1025,7 +1047,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -1100,7 +1122,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -1255,7 +1277,7 @@ public class BinaryMessage extends BitSet
if(!message.matches("[01]*"))
{
throw new IllegalArgumentException(
"Message must contain only zeros and ones");
"Message must contain only zeros and ones");
}
BinaryMessage buffer = new BinaryMessage(message.length());

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bch;
/**
* BCH(63) decoder base implementation that uses GF(2^6) and standard primitive polynomial.
*/
public abstract class BCH_63 extends BCH
{
public static final int M = 6;
/**
* Constructs an instance
* @param k data bits size
* @param t error detection and correction capacity of the generator polynomial used to create the codewords.
*/
public BCH_63(int k, int t)
{
super(M, k, t, PRIMITIVE_POLYNOMIAL_GF_63);
}
}

View File

@@ -0,0 +1,69 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bch;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
/**
* APCO 25 BCH(63,16,23) code with T=11 error bit correction capacity.
*/
public class BCH_63_16_23_P25 extends BCH_63
{
public static final int K = 16;
private static final int T = 11; // Error-correcting capability
public static final IntField NAC_FIELD = IntField.length12(0);
public static final IntField DUID_FIELD = IntField.length4(12);
/**
* Constructs a BCH decoder instance for processing APCO25 BCH(63,16,23) protected NID codewords
*/
public BCH_63_16_23_P25()
{
super(K, T);
}
@Override
public void decode(CorrectedBinaryMessage message)
{
super.decode(message);
}
/**
* Attempts to error correct the NID message. If the message is uncorrectable, overwrite the message NAC field with
* the most frequently/recently observed NAC value and attempt to correct the message a second time.
* @param message to correct
* @param observedNAC that can be used to attempt a second correction on an uncorrectable message.
*/
public void decode(CorrectedBinaryMessage message, int observedNAC)
{
decode(message);
if(message.getCorrectedBitCount() == BCH.MESSAGE_NOT_CORRECTED && observedNAC > 0)
{
//Check to see if the message NAC is different than the observed NAC ... overwrite and try again.
if(message.getInt(NAC_FIELD) != observedNAC)
{
message.setInt(observedNAC, NAC_FIELD);
decode(message);
}
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bch;
/**
* Finds primitive polynomials for a Galois Fields (GF) of size M.
*/
public class GaloisFieldPrimitiveFinder
{
private int M;
private int N;
/**
* Constructs an instance of primitive finder for m for GF(2 ^ m)
* @param m size of galois field
*/
public GaloisFieldPrimitiveFinder(int m)
{
M = m;
N = ((1 << M) - 1);
}
public boolean isPrimitive(int polynomial)
{
int x = 1;
int k = 1 << M;
for(int i = 0; i < N; i++)
{
if(i > 0 && (x == 1))
{
return false;
}
x <<= 1;
if((x & k) > 0)
{
x ^= polynomial;
}
}
return (x == 1);
}
public static void main(String[] args)
{
GaloisFieldPrimitiveFinder finder;
//Search: M=2 to M=9
for(int m = 2; m < 10; m++)
{
finder = new GaloisFieldPrimitiveFinder(m);
System.out.println("Searching for M: " + m);
for(int i = 0; i < (1 << m); i++)
{
//Skip polynomials divisible by X or X+1
int candidate = i | (1 << m);
if(finder.isPrimitive(candidate))
{
System.out.println("M:" + m + " Primitive: " + String.format("0x%X", candidate));
}
}
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bch;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Tests for the BCH decoder for APCO25 NID fragments protected by a BCH(63,16,23) code
*/
public class BCH_63_16_23_P25_Test
{
/**
* NAC: 1, DUID:0 (HDU), line 12 in the FDMA Table 19 NID Generator matrix.
*
* Octal: 00 0020 1430 2667 7236 1044
* Bin: 0 000 000 000 010 000 001 100 011 000 010 110 110 111 111 010 011 110 001 000 100 100
* Bin: 0000 0000 0001 0000 0011 0001 1000 0101 1011 0111 1110 1001 1110 0010 0010 0100
* Hex: 0 0 1 0 3 1 8 5 B 7 E 9 E 2 2 4
* Hex: 00103185B7E9E224
*/
private static final long[] P25_NID_BCH_63_16_GENERATOR_MATRIX = new long[16];
static
{
P25_NID_BCH_63_16_GENERATOR_MATRIX[0] = Long.parseLong("6331141367235452", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[1] = Long.parseLong("5265521614723276", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[2] = Long.parseLong("4603711461164164", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[3] = Long.parseLong("2301744630472072", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[4] = Long.parseLong("7271623073000466", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[5] = Long.parseLong("5605650752635660", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[6] = Long.parseLong("2702724365316730", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[7] = Long.parseLong("1341352172547354", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[8] = Long.parseLong("0560565075263566", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[9] = Long.parseLong("6141333751704220", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[10] = Long.parseLong("3060555764742110", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[11] = Long.parseLong("1430266772361044", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[12] = Long.parseLong("0614133375170422", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[13] = Long.parseLong("6037114611641642", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[14] = Long.parseLong("5326507063515373", 8);
P25_NID_BCH_63_16_GENERATOR_MATRIX[15] = Long.parseLong("4662302756473127", 8);
}
private static final IntField NAC_FIELD = IntField.length12(0);
private static final IntField DUID_FIELD = IntField.length4(12);
/**
* Creates a valid P25 NID 64-bit message containing the NAC and DUID values and calculated parity bits.
* @param nac
* @param duid
* @return
*/
public static CorrectedBinaryMessage create(int nac, int duid)
{
CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(64);
cbm.setInt(nac, NAC_FIELD);
cbm.setInt(duid, DUID_FIELD);
long parity = 0;
for(int x = 0; x < 16; x++)
{
if(cbm.get(x))
{
parity ^= P25_NID_BCH_63_16_GENERATOR_MATRIX[x];
}
}
cbm.load(16, 48, parity);
return cbm;
}
/**
* Verify that the decoder can correct all 1-bit errors across the message bits 0-62. Note: this test ignores the
* unused parity bit at index 63.
* @param decoder that can correct one-bit errors.
* @param original that is fully formed across indices 0-62 with BCH parity bits
* @return true if the test is successful
*/
public static List<Integer> testOneBitErrors(BCH decoder, CorrectedBinaryMessage original, List<Integer> errors)
{
//Induce an additional 1 bit error and test across each bit position.
for(int error = 0; error < 63; error++)
{
if(!errors.contains(error))
{
CorrectedBinaryMessage localCopy = original.getSubMessage(0, 62);
for(int additionalError: errors)
{
localCopy.flip(additionalError);
}
localCopy.flip(error);
decoder.decode(localCopy);
localCopy.xor(original);
if(localCopy.cardinality() > 0)
{
errors.add(error);
return errors;
}
}
}
return Collections.emptyList();
}
public static List<Integer> testMultiBitErrors(BCH decoder, CorrectedBinaryMessage original, List<Integer> errors)
{
System.out.print("\rTesting: " + errors);
if(errors.size() + 2 >= decoder.getMaxErrorCorrection())
{
return Collections.emptyList();
}
for(int error = 0; error < 63; error++)
{
if(!errors.contains(error))
{
List<Integer> errorsCopy = new ArrayList<>(errors);
errorsCopy.add(error);
List<Integer> result = testOneBitErrors(decoder, original, errorsCopy);
if(!result.isEmpty())
{
return result;
}
errorsCopy = new ArrayList<>(errors);
errorsCopy.add(error);
result = testMultiBitErrors(decoder, original, errorsCopy);
if(!result.isEmpty())
{
return result;
}
}
}
return Collections.emptyList();
}
public static void main(String[] args)
{
System.out.println("Starting ....");
BCH decoder = new BCH_63_16_23_P25();
CorrectedBinaryMessage message = create(534, 2);
//Test correcting 1-11 bit errors
message.clear(63); //Clear the parity bit so that it doesn't interfere with the results validation
List<Integer> errors = new ArrayList<>();
List<Integer> results = testMultiBitErrors(decoder, message, errors);
System.out.println("Test Results: " + (results.isEmpty() ? "PASS" : "FAIL") + " Resulse: " + results);
//Test 11 bit errors.
CorrectedBinaryMessage errorMessage = message.getSubMessage(0, 63);
int[] errorArray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for(int error: errorArray)
{
errorMessage.flip(error);
}
decoder.decode(errorMessage);
System.out.println(" Original: " + message);
System.out.println("Corrected: " + errorMessage + " Corrected Bit Count: " + errorMessage.getCorrectedBitCount());
}
}