diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index fc47979f..0b58b69d 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -2,18 +2,24 @@ bahroo + baseband basebanded broadcastify + cach correlator deinterleave descrambled + dibit dinf + dqpsk esnid fleetsync heterdyned heterodynes + hytera icecast ident + lcns nutall overflowable polyphase diff --git a/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java b/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java index c61677f3..e39c79bb 100644 --- a/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java +++ b/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -44,9 +44,9 @@ public class Checksum_5_DMR /** * Indicates if the 77-bit Full Link Control message is valid. * @param message to check - * @return true if the checksum matches. + * @return residual value leftover from xor of the calculated and transmitted checksums. */ - public static boolean isValid(CorrectedBinaryMessage message) + public static int isValid(CorrectedBinaryMessage message) { int accumulator = message.getInt(LC_0); accumulator += message.getInt(LC_1); @@ -57,10 +57,8 @@ public class Checksum_5_DMR accumulator += message.getInt(LC_6); accumulator += message.getInt(LC_7); accumulator += message.getInt(LC_8); - accumulator = accumulator % 31; int checksum = message.getInt(CHECKSUM); - - return accumulator == checksum; + return accumulator ^ checksum; } } diff --git a/src/main/java/io/github/dsheirer/edac/RS_12_9_DMR.java b/src/main/java/io/github/dsheirer/edac/RS_12_9_DMR.java new file mode 100644 index 00000000..ac45faad --- /dev/null +++ b/src/main/java/io/github/dsheirer/edac/RS_12_9_DMR.java @@ -0,0 +1,535 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.edac; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements Reed Solomon RS(12,9,4) error detection and correction. The RS code is based on a shortened GF(8) + * RS(255,252,4) code which can detect and correct up to 1x symbol error. Since each symbol is 8-bits long, we can + * correct up to 8-bit errors, as long as the bit errors are contained within the same 8-bit symbol. + * + * The DMR full link control (FLC) message is composed of 96 bits representing 12x 8-bit symbols where the first + * 9x symbols are message codewords and the 3x trailing are parity symbols. + * + * This class was ported from the original implementation at: + * https://github.com/nonoo/dmrshark/blob/master/libs/coding/rs-12-9.c + * License: dmrshark library uses the same GPL3 license used by sdrtrunk. + * + * Usage: all methods are static. Use the correct(CorrectedBinaryMessage cbm) method to detect and correct errors and + * inspect the cbm.getCorrectedBitCount(). A non-negative value indicates success and the count of bits corrected. + */ +public class RS_12_9_DMR +{ + /** + * Primitive polynomial used to generate the EXPONENTS_TABLE and LOG_TABLE values. See p.142, B.14 + */ + private static final int PRIMITIVE_POLYNOMIAL = 0x11D; + + // See DMR AI. spec. page 142, Table B.19 + private static final int[] EXPONENTS_TABLE = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1D, 0x3A, 0x74, 0xE8, 0xCD, 0x87, 0x13, 0x26, + 0x4C, 0x98, 0x2D, 0x5A, 0xB4, 0x75, 0xEA, 0xC9, 0x8F, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, + 0x9D, 0x27, 0x4E, 0x9C, 0x25, 0x4A, 0x94, 0x35, 0x6A, 0xD4, 0xB5, 0x77, 0xEE, 0xC1, 0x9F, 0x23, + 0x46, 0x8C, 0x05, 0x0A, 0x14, 0x28, 0x50, 0xA0, 0x5D, 0xBA, 0x69, 0xD2, 0xB9, 0x6F, 0xDE, 0xA1, + 0x5F, 0xBE, 0x61, 0xC2, 0x99, 0x2F, 0x5E, 0xBC, 0x65, 0xCA, 0x89, 0x0F, 0x1E, 0x3C, 0x78, 0xF0, + 0xFD, 0xE7, 0xD3, 0xBB, 0x6B, 0xD6, 0xB1, 0x7F, 0xFE, 0xE1, 0xDF, 0xA3, 0x5B, 0xB6, 0x71, 0xE2, + 0xD9, 0xAF, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0D, 0x1A, 0x34, 0x68, 0xD0, 0xBD, 0x67, 0xCE, + 0x81, 0x1F, 0x3E, 0x7C, 0xF8, 0xED, 0xC7, 0x93, 0x3B, 0x76, 0xEC, 0xC5, 0x97, 0x33, 0x66, 0xCC, + 0x85, 0x17, 0x2E, 0x5C, 0xB8, 0x6D, 0xDA, 0xA9, 0x4F, 0x9E, 0x21, 0x42, 0x84, 0x15, 0x2A, 0x54, + 0xA8, 0x4D, 0x9A, 0x29, 0x52, 0xA4, 0x55, 0xAA, 0x49, 0x92, 0x39, 0x72, 0xE4, 0xD5, 0xB7, 0x73, + 0xE6, 0xD1, 0xBF, 0x63, 0xC6, 0x91, 0x3F, 0x7E, 0xFC, 0xE5, 0xD7, 0xB3, 0x7B, 0xF6, 0xF1, 0xFF, + 0xE3, 0xDB, 0xAB, 0x4B, 0x96, 0x31, 0x62, 0xC4, 0x95, 0x37, 0x6E, 0xDC, 0xA5, 0x57, 0xAE, 0x41, + 0x82, 0x19, 0x32, 0x64, 0xC8, 0x8D, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xDD, 0xA7, 0x53, 0xA6, + 0x51, 0xA2, 0x59, 0xB2, 0x79, 0xF2, 0xF9, 0xEF, 0xC3, 0x9B, 0x2B, 0x56, 0xAC, 0x45, 0x8A, 0x09, + 0x12, 0x24, 0x48, 0x90, 0x3D, 0x7A, 0xF4, 0xF5, 0xF7, 0xF3, 0xFB, 0xEB, 0xCB, 0x8B, 0x0B, 0x16, + 0x2C, 0x58, 0xB0, 0x7D, 0xFA, 0xE9, 0xCF, 0x83, 0x1B, 0x36, 0x6C, 0xD8, 0xAD, 0x47, 0x8E, 0x01 + }; + + // See DMR AI. spec. page 143, Table B.20. + private static final int[] LOG_TABLE = {0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224, + 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, + 240, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98, + 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, + 131, 56, 70, 64, 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10, + 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, + 197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63, 209, 91, 149, + 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, + 109, 65, 162, 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, 161, 59, 82, 41, + 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, + 117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175 + }; + + // See DMR AI. spec. page 141, B.10 for these coefficients. + private static final int[] GENERATOR_POLYNOMIAL = {0x40, 0x38, 0x0e, 0x01}; + private static final int CODEWORD_SIZE = 12; + private static final int CHECKSUM_SIZE = 3; //L + private static final int POLYNOMIAL_MAXIMUM_DEGREE = (CHECKSUM_SIZE * 2); + public static final int ERRORS_CANT_BE_CORRECTED = -1; + + /** + * Performs error detection and correction on the Full Link Control (FLC) message. The message should be 96-bits + * long containing 9x 8-bit message symbols and 3x 8-bit parity symbols. The RS(12,9,4) algorithm can detect and + * correct up to 1x 8-bit symbol, or up to 8 bit errors if they are all contained in the same symbol. + * + * Error detection and correction results will be stored in the message.getCorrectedBitCount() and the message will + * be corrected in place, if able to correct. A corrected bit count of ERRORS_CANT_BE_CORRECTED (-1) indicates that + * the message could not be corrected. + * + * @param cbm to correct. + * @param mask to apply/remove before error detection and correction. + */ + public static int correct(CorrectedBinaryMessage cbm, int mask) + { + int[] codeword = new int[12]; + + for(int index = 0; index < 12; index++) + { + codeword[index] = (0xFF & cbm.getByte(index * 8)); + + //Apply the mask to parity symbols 9-11 + if(index >= 9) + { + codeword[index] ^= (mask & 0xFF); + } + } + + int[] syndrome = calculateSyndrome(codeword); + + if(hasErrors(syndrome)) + { + int correctedSymbolIndex = correctErrors(codeword, syndrome); + + if(correctedSymbolIndex >= 0) + { + //Get the previous symbol value and xor to calculate the bit error correction pattern + int previousValue = (0xFF & cbm.getByte(correctedSymbolIndex * 8)); + int correctionPattern = previousValue ^ codeword[correctedSymbolIndex]; + + //Apply the correction to the original message + cbm.setByte(8 * correctedSymbolIndex, (byte)(0xFF & codeword[correctedSymbolIndex])); + + //Set the corrected bit count to the number of ones in the correction pattern from the corrected symbol. + cbm.setCorrectedBitCount(Integer.bitCount(correctionPattern)); + return 0; + } + else + { + //Message can't be corrected - too many errors. + cbm.setCorrectedBitCount(correctedSymbolIndex); + + //Calculate the checksum residual and return that as the observed alternate mask value. Note: the + //DMR ICD checksum has already been applied to the codeword and this calculated residual is the + //'other' masking value that is being used on both Headers and Terminators. + int[] checksum = calculateChecksum(codeword); + + for(int index = 0; index < 3; index++) + { + checksum[index] ^= codeword[index + 9]; + } + + return checksum[0] << 16 | checksum[1] << 8 | checksum[2]; + } + } + else + { + cbm.setCorrectedBitCount(0); + return 0; + } + } + + /** + * Performs GF(8) multiplication (a + b % 255) + * @param factorA factor + * @param factorB factor + * @return product + */ + private static int galoisMultiplication(int factorA, int factorB) + { + if (factorA == 0 || factorB == 0) + { + return 0; + } + + return EXPONENTS_TABLE[((LOG_TABLE[factorA & 0xFF] + LOG_TABLE[factorB & 0xFF]) % 255)]; + } + + /** + * Calculates the inverse. + * @param elt value + * @return inverse + */ + private static int galoisInverse(int elt) + { + return EXPONENTS_TABLE[255 - LOG_TABLE[elt]]; + } + + /** + * Multiplies the polynomial by z, resulting in a shift right by 1 operation. + * @param poly to multiply. + */ + private static void multiplyPolynomialByZ(int[] poly) + { + for(int i = POLYNOMIAL_MAXIMUM_DEGREE - 1; i > 0; i--) + { + poly[i] = poly[i - 1]; + } + + poly[0] = 0; + } + + /** + * Multiplies the polynomials and returns the result. + * @param poly1 argument + * @param poly2 argument + * @return result + */ + private static int[] multiplyPolynomials(int[] poly1, int[] poly2) + { + int[] product = new int[POLYNOMIAL_MAXIMUM_DEGREE * 2]; + + int i; + int j; + int[] tmp1 = new int[POLYNOMIAL_MAXIMUM_DEGREE * 2]; + + for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE * 2; i++) + { + product[i] = 0; + } + + for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++) + { + for(j = POLYNOMIAL_MAXIMUM_DEGREE; j < (POLYNOMIAL_MAXIMUM_DEGREE *2); j++) + { + tmp1[j] = 0; + } + + // Scale tmp1 by p1[i] + for(j = 0; j < POLYNOMIAL_MAXIMUM_DEGREE; j++) + { + tmp1[j] = galoisMultiplication(poly2[j], poly1[i]); + } + + // Shift (multiply) tmp1 right by i + for (j = (POLYNOMIAL_MAXIMUM_DEGREE * 2) - 1; j >= i; j--) + { + tmp1[j] = tmp1[j-i]; + } + + for (j = 0; j < i; j++) + { + tmp1[j] = 0; + } + + // Add into partial product. + for (j = 0; j < (POLYNOMIAL_MAXIMUM_DEGREE *2); j++) + { + product[j] ^= tmp1[j]; + } + } + + return product; + } + + /** + * Calculates the error evaluator polynomial (EEP). Computes the combined erasure/error evaluator polynomial + * (error_locator_poly*syndrome mod z^4 + * @param elp error locator polynomial + * @param syndrome calculated from the codeword + * @return error evaluator polynomial (EEP) + */ + private static int[] calculateEEP(int[] elp, int[] syndrome) + { + int[] eep = new int[POLYNOMIAL_MAXIMUM_DEGREE]; + int[] product = multiplyPolynomials(elp, syndrome); + int i; + + for (i = 0; i < CHECKSUM_SIZE; i++) + { + eep[i] = product[i]; + } + + for (; i < POLYNOMIAL_MAXIMUM_DEGREE; i++) + { + eep[i] = 0; + } + + return eep; + } + + /** + * Computes the discrepancy. + * @param elp error locator polynomial + * @param syndrome from the codeword + * @param L maximum capacity to correct. + * @param n argument + * @return discrepancy + */ + private static int computeDiscrepancy(int[] elp, int[] syndrome, int L, int n) + { + int sum = 0; + + for (int i = 0; i <= L; i++) + { + sum ^= galoisMultiplication(elp[i], syndrome[n - i]); + } + + return sum; + } + + /** + * Finds the coefficients of the error locator polynomial (ELP) and then calculates the error evaluator + * (EEP) using the Berlekamp-Massey algorithm. From Cain, Clark, "Error-Correction Coding For Digital + * Communications", pp. 216. + * @param syndrome calculated from the codeword + * @param elp calculated. + * @return EEP + */ + private static int[] calculateELP(int[] syndrome) + { + int[] elp = new int[POLYNOMIAL_MAXIMUM_DEGREE]; + int[] psi2 = new int[POLYNOMIAL_MAXIMUM_DEGREE]; + int[] bigD = new int[POLYNOMIAL_MAXIMUM_DEGREE]; + + int L = 0; + int L2; + int k = -1; + int d; + int i; + + bigD[1] = 1; + elp[0] = 1; + + for(int n = 0; n < CHECKSUM_SIZE; n++) + { + d = computeDiscrepancy(elp, syndrome, L, n); + + if (d != 0) + { + // psi2 = error_locator_poly - d*D + for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++) + { + psi2[i] = (elp[i] ^ galoisMultiplication(d, bigD[i])); + } + + if (L < (n - k)) + { + L2 = (n - k); + k = (n - L); + + for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++) + { + bigD[i] = galoisMultiplication(elp[i], galoisInverse(d)); + } + + L = L2; + } + + // error_locator_poly = psi2 + for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++) + { + elp[i] = psi2[i]; + } + } + + multiplyPolynomialByZ(bigD); + } + + return elp; + } + + /** + * The error-locator polynomial's roots are found by looking for the values of a^n where evaluating the polynomial + * yields zero (evaluating rs_12_9_error_locator_poly at successive values of alpha (Chien's search)). + * @param elp error locator polynomial. + * @return error location roots. + */ + private static List findRoots(int[] elp) + { + List roots = new ArrayList<>(); + int sum; + int k; + + for(int r = 1; r < 256; r++) + { + sum = 0; + + // Evaluate ELP at r + for(k = 0; k < CHECKSUM_SIZE + 1; k++) + { + sum ^= galoisMultiplication(EXPONENTS_TABLE[(k * r) % 255], elp[k]); + } + + if(sum == 0) + { + roots.add(255 - r); + } + } + + return roots; + } + + /** + * Calculates the syndrome for a codeword. + * @param codeword to check. + * @return syndrome. + */ + private static int[] calculateSyndrome(int[] codeword) + { + if(codeword.length != CODEWORD_SIZE) + { + throw new IllegalArgumentException("Codeword length [" + codeword.length + "] should be " + CODEWORD_SIZE + + " symbols"); + } + + int[] syndrome = new int[POLYNOMIAL_MAXIMUM_DEGREE]; + + int i, temp; + + for(int j = 0; j < 3; j++) + { + for (i = 0; i < codeword.length; i++) + { + temp = galoisMultiplication(EXPONENTS_TABLE[j + 1], syndrome[j]); + temp ^= codeword[i]; + syndrome[j] = (0xFF & temp); + } + } + + return syndrome; + } + + /** + * Checks the syndrome array for non-zero values indicating an error. + * @param syndrome to check + * @return true if the syndrome array has any non-zero values. + */ + private static boolean hasErrors(int[] syndrome) + { + for(int symbol: syndrome) + { + if(symbol != 0) + { + return true; + } + } + + return false; + } + + /** + * Corrects errors in the codeword using the calculated syndrome. + * @param codeword to correct. The codeword will be modified with corrections, if able. + * @param syndrome calculated from the codeword. + * @return -1 if the codeword can't be corrected or the index of the symbol that was corrected. + */ + private static int correctErrors(int[] codeword, int[] syndrome) + { + int r; + int i; + int j; + int errorMask; + int num, denom; + + //Error Locator Polynomial (ELP) + int[] elp = calculateELP(syndrome); + + //Error Evaluator Polynomial (EEP) + int[] eep = calculateEEP(elp, syndrome); + + List roots = findRoots(elp); + + if (roots.isEmpty()) + { + return ERRORS_CANT_BE_CORRECTED; + } + + // Error correction is done using the error-evaluator equation on pp 207. + if (roots.size() <= CHECKSUM_SIZE) + { + // First check for illegal error locations. + for (r = 0; r < roots.size(); r++) + { + if (roots.get(r) >= CODEWORD_SIZE) + { + return ERRORS_CANT_BE_CORRECTED; + } + } + + int errorsCorrected = 0; + + // Evaluates rs_12_9_error_evaluator_poly/rs_12_9_error_locator_poly' at the roots + // alpha^(-i) for error locs i. + for (r = 0; r < roots.size(); r++) + { + i = roots.get(r); + + // Evaluate rs_12_9_error_evaluator_poly at alpha^(-i) + num = 0; + for (j = 0; j < POLYNOMIAL_MAXIMUM_DEGREE; j++) + { + num ^= galoisMultiplication(eep[j], EXPONENTS_TABLE[((255 - i) * j) % 255]); + } + + // Evaluate rs_12_9_error_evaluator_poly' (derivative) at alpha^(-i). All odd powers disappear. + denom = 0; + + for (j = 1; j < POLYNOMIAL_MAXIMUM_DEGREE; j += 2) + { + denom ^= galoisMultiplication(elp[j], EXPONENTS_TABLE[((255-i)*(j-1)) % 255]); + } + + errorMask = galoisMultiplication(num, galoisInverse(denom)); + int index = CODEWORD_SIZE - i - 1; + codeword[index] ^= errorMask; + return index; + } + + return errorsCorrected; + } + + return ERRORS_CANT_BE_CORRECTED; + } + + /** + * Calculates the checksum parity symbols for the codeword using the DMR generator polynomial. + * @param codeword to calculate. + * @return checksum bytes (3) + */ + private static int[] calculateChecksum(int[] codeword) + { + int[] checksum = new int[CHECKSUM_SIZE]; + + int i; + int feedback; + + for (i = 0; i < 9; i++) + { + feedback = (codeword[i] ^ checksum[0]); + checksum[0] = (checksum[1] ^ galoisMultiplication(GENERATOR_POLYNOMIAL[2], feedback)); + checksum[1] = (checksum[2] ^ galoisMultiplication(GENERATOR_POLYNOMIAL[1], feedback)); + checksum[2] = galoisMultiplication(GENERATOR_POLYNOMIAL[0], feedback); + } + + return checksum; + } +} diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java deleted file mode 100644 index 137521b6..00000000 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java +++ /dev/null @@ -1,149 +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 - * **************************************************************************** - */ - -package io.github.dsheirer.edac; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Berlecamp-Massey decoder for Reed Solomon RS(12,9,4) protected messages using DMR generator polynomial - */ -public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR -{ - private final static Logger mLog = LoggerFactory.getLogger(ReedSolomon_12_9_4_DMR.class); - - private static final int[] FLC_CODEWORD_0 = {0, 1, 2, 3, 4, 5, 6, 7}; - private static final int[] FLC_CODEWORD_1 = {8, 9, 10, 11, 12, 13, 14, 15}; - private static final int[] FLC_CODEWORD_2 = {16, 17, 18, 19, 20, 21, 22, 23}; - private static final int[] FLC_CODEWORD_3 = {24, 25, 26, 27, 28, 29, 30, 31}; - private static final int[] FLC_CODEWORD_4 = {32, 33, 34, 35, 36, 37, 38, 39}; - private static final int[] FLC_CODEWORD_5 = {40, 41, 42, 43, 44, 45, 46, 47}; - private static final int[] FLC_CODEWORD_6 = {48, 49, 50, 51, 52, 53, 54, 55}; - private static final int[] FLC_CODEWORD_7 = {56, 57, 58, 59, 60, 61, 62, 63}; - private static final int[] FLC_CODEWORD_8 = {64, 65, 66, 67, 68, 69, 70, 71}; - private static final int[] FLC_PARITY_0 = {72, 73, 74, 75, 76, 77, 78, 79}; - private static final int[] FLC_PARITY_1 = {80, 81, 82, 83, 84, 85, 86, 87}; - private static final int[] FLC_PARITY_2 = {88, 89, 90, 91, 92, 93, 94, 95}; - - /** - * Constructs an instance - */ - public ReedSolomon_12_9_4_DMR() - { - //Note: the RS(12,9,4) is a shortened form of RS(255,252,4) over GF(8) - super(255, 252); - } - - /** - * Attempts to correct the Full Link Control message returning true if there were zero or one correctable errors - * and returning false if there were two or more errors detected. - * - * Note: if one bit error was detected and corrected, the corrected bit count is set to 1 in the message - * - * @param message to be corrected. - * @param crcMask as defined in the TS 102-361-1 Table B.21 - * @return true if the message is correct or if the message was corrected. - */ - public boolean correctFullLinkControl(CorrectedBinaryMessage message, int crcMask) - { - int[] input = new int[255]; - int[] output = new int[255]; - - input[0] = (message.getInt(FLC_PARITY_2) ^ crcMask); - input[1] = (message.getInt(FLC_PARITY_1) ^ crcMask); - input[2] = (message.getInt(FLC_PARITY_0) ^ crcMask); - input[3] = message.getInt(FLC_CODEWORD_8); - input[4] = message.getInt(FLC_CODEWORD_7); - input[5] = message.getInt(FLC_CODEWORD_6); - input[6] = message.getInt(FLC_CODEWORD_5); - input[7] = message.getInt(FLC_CODEWORD_4); - input[8] = message.getInt(FLC_CODEWORD_3); - input[9] = message.getInt(FLC_CODEWORD_2); - input[10] = message.getInt(FLC_CODEWORD_1); - input[11] = message.getInt(FLC_CODEWORD_0); - - try - { - boolean irrecoverableErrors = decode(input, output); - - if(irrecoverableErrors) - { - return false; - } - } - catch(Exception e) - { - return false; - } - - int pointer = 0; - - for(int x = 11; x >= 0; x--) - { - if(output[x] != -1 && (output[x] != input[x])) - { - message.load(pointer, 8, output[x]); - message.setCorrectedBitCount(1); - return true; - } - - pointer += 8; - } - - return true; - } - - public static void main(String[] args) - { - int[] output = new int[255]; - int[] input = new int[255]; - input[0] = 247; - input[1] = 236; - input[2] = 171; - input[3] = 24; - input[4] = 46; - input[5] = 177; - input[6] = 246; - input[7] = 205; - input[8] = 232; - input[9] = 32; - input[10] = 124; - input[11] = 0; - -// input[254] = 247; -// input[253] = 236; -// input[252] = 171; -// input[251] = 24; -// input[250] = 46; -// input[249] = 177; -// input[248] = 246; -// input[247] = 205; -// input[246] = 232; -// input[245] = 32; -// input[244] = 124; -// input[243] = 0; - - ReedSolomon_12_9_4_DMR rs = new ReedSolomon_12_9_4_DMR(); - boolean irrecoverable = rs.decode(input, output); - - mLog.info("Irrecoverable: " + irrecoverable); - } -} diff --git a/src/main/java/io/github/dsheirer/edac/bch/BCH.java b/src/main/java/io/github/dsheirer/edac/bch/BCH.java index e6258958..d2d17d3c 100644 --- a/src/main/java/io/github/dsheirer/edac/bch/BCH.java +++ b/src/main/java/io/github/dsheirer/edac/bch/BCH.java @@ -78,8 +78,8 @@ public abstract class BCH /** * Lookup tables. Note: naming convention is from the original C++ implementation. */ - private int[] a_pow_tab; - private int[] a_log_tab; + public int[] a_pow_tab; + public int[] a_log_tab; private int[] xi_tab; /** diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java b/src/main/java/io/github/dsheirer/edac/bch/BCH_255.java similarity index 53% rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java rename to src/main/java/io/github/dsheirer/edac/bch/BCH_255.java index 2f2e05c9..32439cf8 100644 --- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java +++ b/src/main/java/io/github/dsheirer/edac/bch/BCH_255.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -17,28 +17,30 @@ * **************************************************************************** */ -package io.github.dsheirer.edac; +package io.github.dsheirer.edac.bch; -/** - * Berlekemp Massey decoder for GF(8) using DMR Generator Polynomial - */ -public class ReedSolomon_255_DMR extends BerlekempMassey +import java.util.Arrays; + +public class BCH_255 extends BCH { - /** - * DMR Reed-Solomon code is generated from a Galois Field(2^8) by the polynomial: a8 + a4 + a3 + a2 + a0. - * In binary, this is expressed as: 100011101 which is reversed to big-endian format for this algorithm - * See: TS 102-361-1 paragraph B.3.6 Reed-Solomon(12,9) - */ - public static final int[] DMR_GENERATOR_POLYNOMIAL = {1, 0, 1, 1, 1, 0, 0, 0, 1}; + public static final int M = 8; /** - * Constructs an instance of RS(total symbols, message symbols) - * - * @param n total symbols - * @param k message symbols + * 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 ReedSolomon_255_DMR(int n, int k) + public BCH_255(int k, int t) { - super(8, n, k, DMR_GENERATOR_POLYNOMIAL); +// super(M, k, t, PRIMITIVE_POLYNOMIAL_GF_255); + super(M, k, t, 0x12D); + } + + public static void main(String[] args) + { + BCH_255 b = new BCH_255(M, 1); + + System.out.println("ALOG: " + Arrays.toString(b.a_log_tab)); + System.out.println("APOW: " + Arrays.toString(b.a_pow_tab)); } } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java index 0b060da7..4237db6c 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -296,7 +296,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor TableColumn lsnColumn = new TableColumn("IDs (TS1/TS2)"); lsnColumn.setPrefWidth(225); - lsnColumn.setCellValueFactory(new PropertyValueFactory<>("logicalSlotNumbers")); + lsnColumn.setCellValueFactory(new PropertyValueFactory<>("description")); mTimeslotFrequencyTable.getColumns().addAll(lsnColumn); diff --git a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java index 9fe86daf..076097eb 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java @@ -22,6 +22,7 @@ package io.github.dsheirer.gui.viewer; import com.google.common.eventbus.EventBus; import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.controller.channel.Channel; +import io.github.dsheirer.module.decode.dmr.DMRCrcMaskManager; import io.github.dsheirer.module.decode.dmr.DMRDecoderState; import io.github.dsheirer.module.decode.dmr.DMRHardSymbolProcessor; import io.github.dsheirer.module.decode.dmr.DMRMessageFramer; @@ -80,12 +81,12 @@ public class DmrViewer extends VBox private static final Logger mLog = LoggerFactory.getLogger(DmrViewer.class); private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr"; - private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class); + private final Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class); private Button mSelectFileButton; private Label mSelectedFileLabel; private TableView mMessagePackageTableView; - private ObservableList mMessagePackages = FXCollections.observableArrayList(); - private FilteredList mFilteredMessagePackages = new FilteredList<>(mMessagePackages); + private final ObservableList mMessagePackages = FXCollections.observableArrayList(); + private final FilteredList mFilteredMessagePackages = new FilteredList<>(mMessagePackages); private CheckBox mShowTS0; private CheckBox mShowTS1; private CheckBox mShowTS2; @@ -178,12 +179,13 @@ public class DmrViewer extends VBox public void run() { List messagePackages = new ArrayList<>(); - DMRMessageFramer messageFramer = new DMRMessageFramer(); + DMRCrcMaskManager crcMaskManager = new DMRCrcMaskManager(false); + DMRMessageFramer messageFramer = new DMRMessageFramer(crcMaskManager); messageFramer.start(); DMRHardSymbolProcessor symbolProcessor = new DMRHardSymbolProcessor(messageFramer); DecodeConfigDMR config = new DecodeConfigDMR(); config.setUseCompressedTalkgroups(useCompressed); - DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); + DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config, crcMaskManager); messageFramer.setListener(messageProcessor); MessagePackager messagePackager = new MessagePackager(); diff --git a/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java index 1e95ec90..d0710634 100644 --- a/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -47,6 +47,12 @@ public class DmrTalkerAliasIdentifier extends TalkerAliasIdentifier return Protocol.DMR; } + @Override + public String toString() + { + return "A-" + super.toString(); + } + public static DmrTalkerAliasIdentifier create(String value) { return new DmrTalkerAliasIdentifier(value); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRCrcMaskManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRCrcMaskManager.java index 33c48536..10fa337c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRCrcMaskManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRCrcMaskManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -19,148 +19,179 @@ 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.Date; import java.util.Iterator; import java.util.Map; -import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Manages alternate CRC masks that may be employed in a system. Some vendors employ non-standard CRC checksum masks - * or initial fill values. This utility tracks these non-standard values by observation count and time and accepts - * these alternate mask values to automatically correct or validate messages when those messages employ these - * alternate mask values. Alternate mask values must be observed at least 3x times in a 2-minute window against the - * same message content, in order for the alternate mask value to be used. Tracked values that do not get another - * observation count within a 2-minute window will be ejected from the cache and excluded as a valid CRC mask candidate. - * - * Notes: different alternate CRC mask values can be used independently in each timeslot. Further, I've seen multiple - * CRC mask values being employed within the same timeslot. This indicates that systems can use multiple CRC mask - * 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! + * Tracks alternate CRC masks that may be employed in a system. Some vendors employ non-standard error detection and + * correction checksum masks or initial fill values. This utility tracks these non-standard values by observation count + * and time and accepts these alternate mask values to automatically correct or validate messages when those messages + * employ these alternate mask values. Alternate mask values must be observed at least 2x times in a staleness + * threshold window against the same message type and opcode, in order for the alternate mask value to be accepted as + * valid. Tracked values that do not get another observation count within the staleness time threshold will be + * ejected from the cache and excluded as a valid CRC mask candidate. */ public class DMRCrcMaskManager { - private Logger LOGGER = LoggerFactory.getLogger(DMRCrcMaskManager.class); - private Map mCsbkTrackerMapTS1 = new TreeMap<>(); - private Map mCsbkTrackerMapTS2 = new TreeMap<>(); - private Set mSortedTrackersTS1 = new TreeSet<>(); - private Set mSortedTrackersTS2 = new TreeSet<>(); + /** + * Maximum time age of a tracked (alternate) mask value before it's ejected from the cache. + */ + private static final long CACHE_EJECTION_POLICY_TIME = TimeUnit.MINUTES.toMillis(2); + + /** + * Maximum staleness count before a mask is ejected from the cache. Staleness count is only incremented when the + * specific type and opcode is encountered. + */ + private static final int CACHE_EJECTION_POLICY_COUNT = 20; + + private final Map mCsbkTrackerMap = new TreeMap<>(); + private final Map mCrc5TrackerMap = new TreeMap<>(); + private final Map mRs12_9TrackerMap = new TreeMap<>(); + private final boolean mIgnoreCRCChecksums; /** * Constructs an instance + * @param ignoreCRCChecksums value used to flag all error detection as valid. */ - public DMRCrcMaskManager() + public DMRCrcMaskManager(boolean ignoreCRCChecksums) { + mIgnoreCRCChecksums = ignoreCRCChecksums; } /** - * Logs the current tracked CRC values and counts. + * Indicates if the decoder is configured to ignore invalid error detection and correction checksums. */ - public void log() + public boolean isIgnoreCRCChecksums() { - - StringBuilder sb = new StringBuilder(); - - sb.append("DMR CRC Mask Manager - TS1 Current Tracked Values\n"); - for(MaskTracker tracker: mSortedTrackersTS1) - { - sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue())); - sb.append(" Count:").append(tracker.getObservationCount()); - sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n"); - } - - sb.append("DMR CRC Mask Manager - TS2 Current Tracked Values\n"); - for(MaskTracker tracker: mSortedTrackersTS2) - { - sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue())); - sb.append(" Count:").append(tracker.getObservationCount()); - sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n"); - } - - LOGGER.info(sb.toString()); + return mIgnoreCRCChecksums; } /** - * Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and - * if so, attempts to correct the message. - * @param csbk to re-check using the currently tracked mask value. + * Indicates if the residual value is commonly seen for this opcode. + * @param opcode of the link control message + * @param residual from the RS(12,9) check + * @param timestamp of the message + * @return true if this value is repeatedly seen for this opcode. */ - public void check(CSBKMessage csbk) + public boolean isValidRS12_9(int opcode, int residual, long timestamp) { - if(!csbk.isValid()) + return mIgnoreCRCChecksums || isValid(opcode, residual, timestamp, mRs12_9TrackerMap); + } + + /** + * Indicates if the residual value is commonly seen for this opcode. + * @param opcode of the link control message + * @param residual from the CRC-5 check + * @param timestamp of the message + * @return true if this value is repeatedly seen for this opcode. + */ + public boolean isValidCRC5(int opcode, int residual, long timestamp) + { + return mIgnoreCRCChecksums || isValid(opcode, residual, timestamp, mCrc5TrackerMap); + } + + /** + * Indicates if the residual value is commonly seen for this opcode. + * @param opcode of the link control message + * @param residual from the CRC-5 check + * @param timestamp of the message + * @return true if this value is repeatedly seen for this opcode. + */ + public boolean isValidCSBK(int opcode, int residual, long timestamp) + { + return mIgnoreCRCChecksums || isValid(opcode, residual, timestamp, mCsbkTrackerMap); + } + + /** + * Checks if the opcode and residual mask value have been seen recently, multiple times. + * @param opcode to check + * @param residual from the CRC or RS checksum. + * @param timestamp of the message + * @param trackerMap to check + * @return true if the opcode and residual have been seen recently, multiple times. + */ + private boolean isValid(int opcode, int residual, long timestamp, Map trackerMap) + { + if(opcode >= 0) { - int alternateMask = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80); + OpcodeMaskTracker tracker = trackerMap.get(opcode); - Map map; - Set sortedSet; - - if(csbk.getTimeslot() == 1) + if(tracker != null) { - map = mCsbkTrackerMapTS1; - sortedSet = mSortedTrackersTS1; + return tracker.isValid(residual, timestamp); } else { - map = mCsbkTrackerMapTS2; - sortedSet = mSortedTrackersTS2; + trackerMap.put(opcode, new OpcodeMaskTracker(residual, timestamp)); } + } - if(map.containsKey(alternateMask)) + return false; + } + + /** + * Tracks a set of mask values for an opcode value. + */ + public static class OpcodeMaskTracker + { + private final Map mTrackerMap = new TreeMap<>(); + + /** + * Constructs an instance + * @param mask to track for the opcode + * @param timestamp when the mask was detected, for tracking staleness + */ + public OpcodeMaskTracker(int mask, long timestamp) + { + mTrackerMap.put(mask, new MaskTracker(timestamp)); + } + + /** + * Indicates if the mask is valid by checking for multiple recent usage. + * @param mask to check + * @param timestamp when the mask was used + * @return true if the mask has been seen multiple times recently + */ + public boolean isValid(int mask, long timestamp) + { + boolean valid = false; + + if(mTrackerMap.containsKey(mask)) { - MaskTracker matchingTracker = map.get(alternateMask); - matchingTracker.increment(csbk.getTimestamp()); - - if(matchingTracker.isValid()) - { - csbk.checkCRC(matchingTracker.getTrackedValue()); - } + MaskTracker matchingTracker = mTrackerMap.get(mask); + matchingTracker.increment(timestamp); + valid = matchingTracker.isValid(); } else { - //Attempt to use other tracked mask values in order of observed count and also remove stale trackers - boolean found = false; - MaskTracker next; - Iterator it = sortedSet.iterator(); + mTrackerMap.put(mask, new MaskTracker(timestamp)); + } + + Iterator> it = mTrackerMap.entrySet().iterator(); + { while(it.hasNext()) { - next = it.next(); - - //If the tracker is stale, remove it - if(next.isStale(csbk.getTimestamp())) + if(it.next().getValue().isStale(timestamp)) { it.remove(); - map.remove(next.getTrackedValue()); } - else - { - //If we haven't found a match yet, attempt to correct the message using this tracked mask value - if(!found) - { - csbk.checkCRC(next.getTrackedValue()); - - if(csbk.isValid()) - { - found = true; - next.increment(csbk.getTimestamp()); - } - } - } - } - - //If we didn't find a perfect match or even a close match (with 1 bit error), create a new tracked value. - if(!found) - { - MaskTracker tracker = new MaskTracker(alternateMask, csbk.getTimestamp()); - map.put(alternateMask, tracker); - sortedSet.add(tracker); } } + + return valid; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + for(Map.Entry entry: mTrackerMap.entrySet()) + { + sb.append("\t").append("Mask [").append(entry.getKey()).append("] ").append(entry.getValue()).append("\n"); + } + return sb.toString(); } } @@ -168,11 +199,8 @@ public class DMRCrcMaskManager * Alternate CRC mask value tracker. Tracks the number of observances of the mask value to detect when an alternate * CRC mask pattern is employed and can then be used to automatically correct CRC values for messages. */ - public class MaskTracker implements Comparable + public static class MaskTracker implements Comparable { - private static final long STALENESS_TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(2); - private static final int STALENESS_COUNT_THRESHOLD = 100; - private int mTrackedValue; private int mObservationCount; private int mStalenessCount; private long mLastUpdated; @@ -180,16 +208,20 @@ public class DMRCrcMaskManager /** * Constructs an instance for the specified mask, sets the observation count to one and sets the last update to * the supplied timestamp. - * @param residual value to track. * @param timestamp for the observation */ - public MaskTracker(int residual, long timestamp) + public MaskTracker(long timestamp) { - mTrackedValue = residual; mObservationCount = 1; mLastUpdated = timestamp; } + @Override + public String toString() + { + return "Observed:" + mObservationCount + " Staleness:" + mStalenessCount; + } + /** * Count of observations for this mask value. * @return count @@ -200,13 +232,13 @@ public class DMRCrcMaskManager } /** - * Indicates that the residual value has been observed at least 3x times within the preceding 2 minute window, + * Indicates that the residual value has been observed at least 3x times within the preceding 2-minute window, * indicating that it is (likely) valid. * @return true if the tracked mask value is valid. */ public boolean isValid() { - return mObservationCount >= 3; + return mObservationCount >= 2; } /** @@ -219,8 +251,8 @@ public class DMRCrcMaskManager public boolean isStale(long timestamp) { mStalenessCount++; - return mStalenessCount > STALENESS_COUNT_THRESHOLD || - Math.abs(timestamp - mLastUpdated) > STALENESS_TIME_THRESHOLD; + return mStalenessCount > CACHE_EJECTION_POLICY_COUNT || + Math.abs(timestamp - mLastUpdated) > CACHE_EJECTION_POLICY_TIME; } /** @@ -231,15 +263,6 @@ public class DMRCrcMaskManager return mLastUpdated; } - /** - * Tracked alternate mask value. - * @return tracked mask value - */ - public int getTrackedValue() - { - return mTrackedValue; - } - /** * Increment the observation count and use the supplied timestamp as the last updated timestamp. * @param timestamp for the observed message. @@ -248,7 +271,7 @@ public class DMRCrcMaskManager { mStalenessCount = 0; mObservationCount++; - //Detect integer rollover and apply max integer value. + //Detect integer rollover and adjust to max integer value. if(mObservationCount < 0) { mObservationCount = Integer.MAX_VALUE; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java index 7a57a3fe..6eca1747 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java @@ -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 @@ -86,12 +86,12 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex private static final int SYMBOL_RATE = 4800; private static final Map BASEBAND_FILTERS = new HashMap<>(); private DQPSKDemodulator mDemodulator; - private DMRMessageFramer mMessageFramer = new DMRMessageFramer(); - private DMRSoftSymbolProcessor mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer); - private DMRMessageProcessor mMessageProcessor; + private final DMRMessageFramer mMessageFramer; + private final DMRSoftSymbolProcessor mSymbolProcessor; + private final DMRMessageProcessor mMessageProcessor; private IRealFilter mIBasebandFilter; private IRealFilter mQBasebandFilter; - private PowerMonitor mPowerMonitor = new PowerMonitor(); + private final PowerMonitor mPowerMonitor = new PowerMonitor(); /** * Constructs an instance @@ -99,7 +99,10 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex */ public DMRDecoder(DecodeConfigDMR config, boolean isTrafficChannel) { - mMessageProcessor = new DMRMessageProcessor(config); + DMRCrcMaskManager crcMaskManager = new DMRCrcMaskManager(config.getIgnoreCRCChecksums()); + mMessageFramer = new DMRMessageFramer(crcMaskManager); + mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer); + mMessageProcessor = new DMRMessageProcessor(config, crcMaskManager); mMessageProcessor.setMessageListener(getMessageListener()); setSampleRate(25000.0); @@ -259,11 +262,9 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex */ private void process(SourceEvent sourceEvent) { - switch(sourceEvent.getEvent()) + if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) { - case NOTIFICATION_SAMPLE_RATE_CHANGE: - setSampleRate(sourceEvent.getValue().doubleValue()); - break; + setSampleRate(sourceEvent.getValue().doubleValue()); } } @@ -329,10 +330,6 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex errors = burst.getMessage().getCorrectedBitCount(); mTotalMessageCounter++; - if(mTotalMessageCounter == 492) - { - int a = 0; - } if(burst.isValid()) { mBitErrorCounter += Math.max(errors, 0); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index d7a59d5d..8e119327 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -150,9 +150,9 @@ public class DMRDecoderState extends TimeslotDecoderState } //For RAS protected systems, allows user to ignore CRC checksums and still decode the system - if(channel.getDecodeConfiguration() instanceof DecodeConfigDMR) + if(channel.getDecodeConfiguration() instanceof DecodeConfigDMR config) { - mIgnoreCRCChecksums = ((DecodeConfigDMR)channel.getDecodeConfiguration()).getIgnoreCRCChecksums(); + mIgnoreCRCChecksums = config.getIgnoreCRCChecksums(); } } @@ -857,7 +857,10 @@ public class DMRDecoderState extends TimeslotDecoderState //Channel rotation monitor normally uses only CONTROL state, so when we detect that we're a //Capacity plus system, add ACTIVE as an active state to the monitor. This can be requested repeatedly. - getInterModuleEventBus().post(CAPACITY_PLUS_ACTIVE_STATE_REQUEST); + if(getInterModuleEventBus() != null) + { + getInterModuleEventBus().post(CAPACITY_PLUS_ACTIVE_STATE_REQUEST); + } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java index 54db953d..d5b3c731 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java @@ -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 @@ -35,7 +35,7 @@ import java.util.Arrays; * 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. */ @@ -44,8 +44,8 @@ public class DMRMessageFramer implements Listener private static final int DIBIT_CACH_START = 0; private static final int DIBIT_BURST_END = 144; private Listener mMessageListener; - private Dibit[] mBufferA = new Dibit[144]; - private Dibit[] mBufferB = new Dibit[144]; + private final Dibit[] mBufferA = new Dibit[144]; + private final Dibit[] mBufferB = new Dibit[144]; private int mBufferAPointer = 0; private int mBufferBPointer = 0; private DMRSyncPattern mBufferAPattern = DMRSyncPattern.UNKNOWN; @@ -58,14 +58,16 @@ public class DMRMessageFramer implements Listener private int mDibitSinceTimestampCounter = 0; private long mReferenceTimestamp = 0; private boolean mRunning = false; + private final DMRMessageFactory mMessageFactory; /** * Constructs an instance */ - public DMRMessageFramer() + public DMRMessageFramer(DMRCrcMaskManager crcMaskManager) { Arrays.fill(mBufferA, Dibit.D00_PLUS_1); Arrays.fill(mBufferB, Dibit.D00_PLUS_1); + mMessageFactory = new DMRMessageFactory(crcMaskManager); } /** @@ -138,7 +140,7 @@ public class DMRMessageFramer implements Listener mDibitCounter = 0; - CorrectedBinaryMessage message = getMessage(mBufferA, DIBIT_CACH_START, DIBIT_BURST_END); + CorrectedBinaryMessage message = getMessage(mBufferA); CACH cach = CACH.getCACH(message); if(mBufferAPattern.hasCACH() && cach.isValid() && mBufferATimeslot != cach.getTimeslot()) @@ -156,7 +158,7 @@ public class DMRMessageFramer implements Listener } } - dispatch(DMRMessageFactory.create(mBufferAPattern, message, cach, getTimestamp(), mBufferATimeslot)); + dispatch(mMessageFactory.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. @@ -206,7 +208,7 @@ public class DMRMessageFramer implements Listener mDibitCounter = 0; - CorrectedBinaryMessage burst = getMessage(mBufferB, DIBIT_CACH_START, DIBIT_BURST_END); + CorrectedBinaryMessage burst = getMessage(mBufferB); CACH cach = CACH.getCACH(burst); if(mBufferBPattern.hasCACH() && cach.isValid() && mBufferBTimeslot != cach.getTimeslot()) @@ -225,7 +227,7 @@ public class DMRMessageFramer implements Listener } } - dispatch(DMRMessageFactory.create(mBufferBPattern, burst, cach, getTimestamp(), mBufferBTimeslot)); + dispatch(mMessageFactory.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. @@ -249,7 +251,7 @@ public class DMRMessageFramer implements Listener /** * Dispatches the message to the registered message listener. - * @param message + * @param message to dispatch */ private void dispatch(IMessage message) { @@ -286,17 +288,16 @@ public class DMRMessageFramer implements 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) + private CorrectedBinaryMessage getMessage(Dibit[] buffer) { - CorrectedBinaryMessage message = new CorrectedBinaryMessage(2 * (end - start)); + CorrectedBinaryMessage message = new CorrectedBinaryMessage(2 * (DMRMessageFramer.DIBIT_BURST_END - DMRMessageFramer.DIBIT_CACH_START)); - Dibit dibit = null; - for(int i = start; i < end; i++) + Dibit dibit; + for(int i = DMRMessageFramer.DIBIT_CACH_START; i < DMRMessageFramer.DIBIT_BURST_END; i++) { dibit = buffer[i]; message.add(dibit.getBit1(), dibit.getBit2()); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java index f0d9cbdc..23fd9e71 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java @@ -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 @@ -18,6 +18,7 @@ */ package io.github.dsheirer.module.decode.dmr; +import io.github.dsheirer.edac.CRCDMR; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; @@ -63,28 +64,31 @@ import org.slf4j.LoggerFactory; public class DMRMessageProcessor implements Listener { private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class); - private DecodeConfigDMR mConfigDMR; - private VoiceSuperFrameProcessor mSuperFrameProcessor1 = new VoiceSuperFrameProcessor(); - private VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor(); - private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1); - private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2); - private MBCAssembler mMBCAssembler; - private PacketSequenceAssembler mPacketSequenceAssembler; - private SLCAssembler mSLCAssembler = new SLCAssembler(); - private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler(); + private final DecodeConfigDMR mConfigDMR; + private final VoiceSuperFrameProcessor mSuperFrameProcessor1 = new VoiceSuperFrameProcessor(); + private final VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor(); + private final FLCAssembler mFLCAssemblerTimeslot1; + private final FLCAssembler mFLCAssemblerTimeslot2; + private final MBCAssembler mMBCAssembler; + private final PacketSequenceAssembler mPacketSequenceAssembler; + private final SLCAssembler mSLCAssembler = new SLCAssembler(); + private final TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler(); private Listener mMessageListener; - private Map mTimeslotFrequencyMap = new TreeMap<>(); - private DMRCrcMaskManager mCrcMaskManager = new DMRCrcMaskManager(); - private boolean mIgnoreCrcChecksums; + private final Map mTimeslotFrequencyMap = new TreeMap<>(); + private final DMRCrcMaskManager mCrcMaskManager; /** * Constructs an instance + * @param config for the DMR decoder + * @param crcMaskManager for message error detection and correction. */ - public DMRMessageProcessor(DecodeConfigDMR config) + public DMRMessageProcessor(DecodeConfigDMR config, DMRCrcMaskManager crcMaskManager) { mConfigDMR = config; - mIgnoreCrcChecksums = config.getIgnoreCRCChecksums(); - mMBCAssembler = new MBCAssembler(mIgnoreCrcChecksums); + mCrcMaskManager = crcMaskManager; + mMBCAssembler = new MBCAssembler(crcMaskManager.isIgnoreCRCChecksums()); + mFLCAssemblerTimeslot1 = new FLCAssembler(1, crcMaskManager); + mFLCAssemblerTimeslot2 = new FLCAssembler(2, crcMaskManager); for(TimeslotFrequency timeslotFrequency: config.getTimeslotMap()) { @@ -102,7 +106,7 @@ public class DMRMessageProcessor implements Listener */ private boolean isValid(IMessage message) { - return mIgnoreCrcChecksums || message.isValid(); + return mCrcMaskManager.isIgnoreCRCChecksums() || message.isValid(); } /** @@ -123,10 +127,12 @@ public class DMRMessageProcessor implements Listener mFLCAssemblerTimeslot2.reset(); } - //Detect and correct messages employing an alternate CRC mask pattern. - if(!message.isValid() && message instanceof CSBKMessage csbk) + //Detect and correct messages employing an alternate CRC mask pattern (ie RAS) when ignore CRC is disabled + if(message instanceof CSBKMessage csbk && !message.isValid()) { - mCrcMaskManager.check(csbk); + int opcode = csbk.getOpcode().getValue(); + int residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80); + csbk.setValid(mCrcMaskManager.isValidCSBK(opcode, residual, csbk.getTimestamp())); } if(message instanceof FullLCMessage flc) @@ -199,10 +205,8 @@ public class DMRMessageProcessor implements Listener } } //Extract the Short Link Control message fragment from the DMR burst message when it has one - else if(message instanceof DMRBurst) + else if(message instanceof DMRBurst dmrBurst) { - DMRBurst dmrBurst = (DMRBurst)message; - if(dmrBurst.hasCACH()) { CACH cach = dmrBurst.getCACH(); @@ -272,9 +276,8 @@ public class DMRMessageProcessor implements Listener */ private void enrich(IMessage message) { - if(message instanceof ITimeslotFrequencyReceiver) + if(message instanceof ITimeslotFrequencyReceiver receiver) { - ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message; int[] lcns = receiver.getLogicalChannelNumbers(); List timeslotFrequencies = new ArrayList<>(); @@ -316,8 +319,8 @@ public class DMRMessageProcessor implements Listener } catch(Exception e) { - mLog.error("Error applying compressed talkgroup setting to identifier in message: " + message + - " [" + message.getClass() + "]"); + mLog.error("Error applying compressed talkgroup setting to identifier in message: {} [{}]", + message, message.getClass()); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java index a83d730e..bcfaff99 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/TimeslotFrequency.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -28,6 +28,8 @@ import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.util.Callback; /** @@ -40,6 +42,7 @@ public class TimeslotFrequency private LongProperty mUplinkFrequencyProperty = new SimpleLongProperty(); private DoubleProperty mDownlinkMhzProperty = new SimpleDoubleProperty(); private DoubleProperty mUplinkMhzProperty = new SimpleDoubleProperty(); + private StringProperty mDescriptionProperty = new SimpleStringProperty(); /** * Constructs an instance @@ -60,6 +63,12 @@ public class TimeslotFrequency return copy; } + @JsonIgnore + public StringProperty descriptionProperty() + { + return mDescriptionProperty; + } + /** * Logical Slot Number property */ @@ -122,7 +131,7 @@ public class TimeslotFrequency } @JsonIgnore - public String getLogicalSlotNumbers() + public void updateDescription() { StringBuilder sb = new StringBuilder(); @@ -148,7 +157,25 @@ public class TimeslotFrequency sb.append("(empty)"); } - return sb.toString(); + setDescription(sb.toString()); + } + + /** + * Description of the logical slot numbers for the channel number. + */ + @JsonIgnore + public String getDescription() + { + return mDescriptionProperty.get(); + } + + /** + * Sets the description of the timeslots for the channel number. + * @param description of the logical timeslots for the channel number. + */ + public void setDescription(String description) + { + mDescriptionProperty.set(description); } /** @@ -158,6 +185,7 @@ public class TimeslotFrequency public void setNumber(int lcn) { mNumberProperty.set(lcn); + updateDescription(); } /** @@ -212,6 +240,6 @@ public class TimeslotFrequency public static Callback extractor() { return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyProperty(), - tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()}; + tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz(), tf.descriptionProperty()}; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java index cc898e11..2ad42c8a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/DMRMessageFactory.java @@ -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 @@ -22,6 +22,7 @@ package io.github.dsheirer.module.decode.dmr.message; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.message.EmptyTimeslotPlaceholderMessage; import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.dmr.DMRCrcMaskManager; 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; @@ -31,9 +32,22 @@ import io.github.dsheirer.protocol.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * DMR Message Factory class for creating messages from DMR bursts. + */ public class DMRMessageFactory { private final static Logger mLog = LoggerFactory.getLogger(DMRMessageFactory.class); + private DMRDataMessageFactory mDataMessageFactory; + + /** + * Constructs an instance + * @param crcMaskManager for error detection and correction. + */ + public DMRMessageFactory(DMRCrcMaskManager crcMaskManager) + { + mDataMessageFactory = new DMRDataMessageFactory(crcMaskManager); + } /** * Creates a DMR message wrapper for the binary message. DMR message type is specified by the sync pattern. @@ -43,8 +57,8 @@ public class DMRMessageFactory * @param timeslot for the burst * @return DMRMessage instance or null for UNKNOWN sync pattern */ - public static IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach, - long timestamp, int timeslot) + public IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach, long timestamp, + int timeslot) { switch(syncPattern) { @@ -60,11 +74,11 @@ public class DMRMessageFactory return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 2); case BASE_STATION_DATA: case MOBILE_STATION_DATA: - return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot); + return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot); case DIRECT_DATA_TIMESLOT_1: - return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1); + return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1); case DIRECT_DATA_TIMESLOT_2: - return DMRDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2); + return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2); case DIRECT_EMPTY_TIMESLOT: return new EmptyTimeslotPlaceholderMessage(timestamp, 288, Protocol.DMR, timeslot); case UNKNOWN: diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java index e3da332a..480cc020 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DMRDataMessageFactory.java @@ -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 @@ -23,6 +23,7 @@ import io.github.dsheirer.bits.BitSetFullException; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.edac.CRCDMR; import io.github.dsheirer.edac.trellis.ViterbiDecoder_3_4_DMR; +import io.github.dsheirer.module.decode.dmr.DMRCrcMaskManager; 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; @@ -45,6 +46,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader; +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.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.usb.USBData; @@ -62,6 +65,16 @@ public class DMRDataMessageFactory { private final static Logger mLog = LoggerFactory.getLogger(DMRDataMessageFactory.class); private static final ViterbiDecoder_3_4_DMR VITERBI_DECODER = new ViterbiDecoder_3_4_DMR(); + private final LCMessageFactory mLinkControlMessageFactory; + + /** + * Constructs an instance + * @param maskManager for managing CSBK and link control error detection. + */ + public DMRDataMessageFactory(DMRCrcMaskManager maskManager) + { + mLinkControlMessageFactory = new LCMessageFactory(maskManager); + } /** * Creates a data message class @@ -72,8 +85,8 @@ public class DMRDataMessageFactory * @param timeslot for the message * @return data message instance */ - public static DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, long timestamp, - int timeslot) + public DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, long timestamp, + int timeslot) { SlotType slotType = SlotType.getSlotType(message); @@ -107,18 +120,12 @@ public class DMRDataMessageFactory } return csbk; case USB_DATA: - CorrectedBinaryMessage usbdPayload = payload; - switch(USBData.getServiceType(usbdPayload)) + DataMessage usb = new USBData(pattern, payload, cach, slotType, timestamp, timeslot); + if(payload.getCorrectedBitCount() < 0) { - case LIP_SHORT_LOCATION_REQUEST: - default: - DataMessage usb =new USBData(pattern, payload, cach, slotType, timestamp, timeslot); - if(payload.getCorrectedBitCount() < 0) - { - usb.setValid(false); - } - return usb; + usb.setValid(false); } + return usb; case MBC_ENC_HEADER: case MBC_HEADER: DataMessage mbc = new MBCHeader(pattern, payload, cach, slotType, timestamp, timeslot); @@ -129,14 +136,18 @@ public class DMRDataMessageFactory return mbc; case CHANNEL_CONTROL_ENC_HEADER: case PI_HEADER: - DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot); + LCMessage piLinkControl = mLinkControlMessageFactory.createFullEncryption(payload, timestamp, + timeslot); + DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot, piLinkControl); if(payload.getCorrectedBitCount() < 0) { pi.setValid(false); } return pi; case VOICE_HEADER: - DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot); + LCMessage vhLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, false); + DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot, + vhLinkControl); if(payload.getCorrectedBitCount() < 0) { voice.setValid(false); @@ -151,7 +162,8 @@ public class DMRDataMessageFactory switch(dpf) { case CONFIRMED_DATA_PACKET: - ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType, timestamp, timeslot); + ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType, + timestamp, timeslot); cdh.setValid(valid); return cdh; case PROPRIETARY_DATA_PACKET: @@ -231,7 +243,8 @@ public class DMRDataMessageFactory return dh; } case TLC: - DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot); + LCMessage tlcLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, true); + DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot, tlcLinkControl); if(payload.getCorrectedBitCount() < 0) { tlc.setValid(false); @@ -285,8 +298,7 @@ public class DMRDataMessageFactory /** * De-scramble, decode and error check a BPTC protected raw message and return a 96-bit error corrected * payload or null. - * @param message - * @return + * @param message containing a payload */ private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message) { diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java index 48d1b806..79d24002 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessage.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -69,6 +69,14 @@ public abstract class DataMessage extends DMRBurst return getMessage().getInt(BPTC_RESERVED_BITS); } + /** + * Value of the reserved bits. Normally this is 2 for RAS enabled systems. + */ + public int getRAS() + { + return getBPTCReservedBits(); + } + /** * Indicates if the BPTC reserved bits value is anything other than 0. * @return true if non-zero. diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java index 702f437f..6ac16607 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/DataMessageWithLinkControl.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -23,8 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.identifier.Identifier; 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; @@ -45,11 +43,13 @@ public abstract class DataMessageWithLinkControl extends DataMessage * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl message */ public DataMessageWithLinkControl(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, - SlotType slotType, long timestamp, int timeslot) + SlotType slotType, long timestamp, int timeslot, LCMessage linkControl) { super(syncPattern, message, cach, slotType, timestamp, timeslot); + mLCMessage = linkControl; } /** @@ -57,12 +57,6 @@ public abstract class DataMessageWithLinkControl extends DataMessage */ public LCMessage getLCMessage() { - if(mLCMessage == null) - { - boolean isTerminator = getSlotType().getDataType() == DataType.TLC; - mLCMessage = LCMessageFactory.createFull(getMessage(), getTimestamp(), getTimeslot(), isTerminator); - } - return mLCMessage; } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java index f2adb5cc..7d1bf482 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxAdvantageModeVoiceChannelUpdate.java @@ -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 @@ -258,42 +258,19 @@ public class CapacityMaxAdvantageModeVoiceChannelUpdate extends CSBKMessage impl @Override public void apply(List timeslotFrequencies) { - if(hasChannel1Timeslot1()) - { - getChannel1TS1().apply(timeslotFrequencies); - } - if(hasChannel1Timeslot2()) - { - getChannel1TS2().apply(timeslotFrequencies); - } - if(hasChannel2Timeslot1()) - { - getChannel2TS1().apply(timeslotFrequencies); - } - if(hasChannel2Timeslot2()) - { - getChannel2TS2().apply(timeslotFrequencies); - } + getChannel1TS1().apply(timeslotFrequencies); + getChannel1TS2().apply(timeslotFrequencies); + getChannel2TS1().apply(timeslotFrequencies); + getChannel2TS2().apply(timeslotFrequencies); } @Override public int[] getLogicalChannelNumbers() { - //Since both timeslots share the same LCN, we use just TS1's channel for both channel 1 and channel 2 - if((hasChannel1Timeslot1() || hasChannel1Timeslot2()) && (hasChannel2Timeslot1() || hasChannel2Timeslot2())) - { - return new int[]{getChannel1TS1().getChannelId(), getChannel2TS1().getChannelId()}; - } - else if((hasChannel1Timeslot1() || hasChannel1Timeslot2())) - { - return new int[]{getChannel1TS1().getChannelId()}; - } - else if((hasChannel2Timeslot1() || hasChannel2Timeslot2())) - { - return new int[]{getChannel2TS1().getChannelId()}; - } - - return getChannel1TS1().getLogicalChannelNumbers(); + return new int[]{getChannel1TS1().getChannelNumber(), + getChannel1TS2().getChannelNumber(), + getChannel2TS1().getChannelNumber(), + getChannel2TS2().getChannelNumber()}; } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java index 745d5d38..b6c6128f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/motorola/CapacityMaxOpenModeVoiceChannelUpdate.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -176,36 +176,14 @@ public class CapacityMaxOpenModeVoiceChannelUpdate extends CSBKMessage implement @Override public void apply(List timeslotFrequencies) { - if(hasTimeslot1()) - { - getChannelTS1().apply(timeslotFrequencies); - } - if(hasTimeslot2()) - { - getChannelTS2().apply(timeslotFrequencies); - } + getChannelTS1().apply(timeslotFrequencies); + getChannelTS2().apply(timeslotFrequencies); } @Override public int[] getLogicalChannelNumbers() { - boolean ts1 = hasTimeslot1(); - boolean ts2 = hasTimeslot2(); - - if(ts1 && ts2) - { - return new int[]{getChannelTS1().getChannelNumber(), getChannelTS2().getChannelNumber()}; - } - else if(ts1) - { - return new int[]{getChannelTS1().getChannelNumber()}; - } - else if(ts2) - { - return new int[]{getChannelTS2().getChannelNumber()}; - } - - return new int[0]; + return new int[]{getChannelTS1().getChannelNumber(), getChannelTS2().getChannelNumber()}; } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java index e029b28e..16091042 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/DataHeader.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -42,11 +42,12 @@ public class DataHeader extends HeaderMessage * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl message from the payload */ public DataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) { - super(syncPattern, message, cach, slotType, timestamp, timeslot); + super(syncPattern, message, cach, slotType, timestamp, timeslot, null); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java index ce38dd41..1f63df23 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/HeaderMessage.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl; import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern; /** @@ -39,10 +40,11 @@ public class HeaderMessage extends DataMessageWithLinkControl * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl message from the payload */ public HeaderMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, - long timestamp, int timeslot) + long timestamp, int timeslot, LCMessage linkControl) { - super(syncPattern, message, cach, slotType, timestamp, timeslot); + super(syncPattern, message, cach, slotType, timestamp, timeslot, linkControl); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PiHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PiHeader.java index 764d42d6..38161eea 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PiHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/PiHeader.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; 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.lc.LCMessage; -import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory; import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern; /** @@ -40,24 +39,12 @@ public class PiHeader extends HeaderMessage * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl message from payload */ - public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) + public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, + long timestamp, int timeslot, LCMessage linkControl) { - super(syncPattern, message, cach, slotType, timestamp, timeslot); - } - - /** - * Access the embedded link control message - */ - public LCMessage getLCMessage() - { - if(mLCMessage == null) - { - //Overrides default to ignore opcode and construct an encryption parameters message. - mLCMessage = LCMessageFactory.createFullEncryption(getMessage(), getTimestamp(), getTimeslot()); - } - - return mLCMessage; + super(syncPattern, message, cach, slotType, timestamp, timeslot, linkControl); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java index 810ebbf9..cef35ba1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/VoiceHeader.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -20,9 +20,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.header; import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.module.decode.dmr.sync.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.lc.LCMessage; +import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern; /** * Voice Header data message. @@ -38,17 +39,22 @@ public class VoiceHeader extends HeaderMessage * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl payload message */ public VoiceHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, - long timestamp, int timeslot) + long timestamp, int timeslot, LCMessage linkControl) { - super(syncPattern, message, cach, slotType, timestamp, timeslot); + super(syncPattern, message, cach, slotType, timestamp, timeslot, linkControl); } @Override public String toString() { StringBuilder sb = new StringBuilder(); + if(hasRAS()) + { + sb.append("RAS:").append(getBPTCReservedBits()).append(" "); + } sb.append("CC:").append(getSlotType().getColorCode()); sb.append(" VOICE HEADER"); sb.append(" ").append(getLCMessage()); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java index 2e9c03d1..b5cc3f4d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java @@ -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 @@ -22,7 +22,8 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.edac.CRCDMR; import io.github.dsheirer.edac.Checksum_5_DMR; -import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR; +import io.github.dsheirer.edac.RS_12_9_DMR; +import io.github.dsheirer.module.decode.dmr.DMRCrcMaskManager; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation; @@ -63,9 +64,18 @@ import org.slf4j.LoggerFactory; public class LCMessageFactory { private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class); - private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR(); private static final int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99; private static final int VOICE_LINK_CONTROL_CRC_MASK = 0x96; + private final DMRCrcMaskManager mMaskManager; + + /** + * Constructs an instance + * @param maskManager to check and/or override error detection + */ + public LCMessageFactory(DMRCrcMaskManager maskManager) + { + mMaskManager = maskManager; + } /** * Creates a full link control message specifically for the PI_HEADER message type. @@ -74,7 +84,7 @@ public class LCMessageFactory * @param timeslot of the original message * @return encryption parameters link control. */ - public static FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot) + public FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot) { return new EncryptionParameters(message, timestamp, timeslot); } @@ -87,28 +97,27 @@ public class LCMessageFactory * @param isTerminator set to true if this is LC for a terminator data burst. * @return message class */ - public static FullLCMessage createFull(CorrectedBinaryMessage message, long timestamp, int timeslot, boolean isTerminator) + public FullLCMessage createFull(CorrectedBinaryMessage message, long timestamp, int timeslot, boolean isTerminator) { if(message == null) { throw new IllegalArgumentException("Message cannot be null"); } - boolean valid = true; + int residual = 0; if(message.size() == 77) { - valid = Checksum_5_DMR.isValid(message); + residual = Checksum_5_DMR.isValid(message); } else if(message.size() == 96) { - //RS(12,9,4) can correct up to floor(4/2) = 1 bit error. - valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, - isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK); + //RS(12,9,4) can correct up to floor((12-9)/2) = 1 symbol (or up to 8 bits if they are in the same symbol). + residual = RS_12_9_DMR.correct(message, isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK); } else { - mLog.warn("Unrecognized Link Control Message Size: " + message.size()); + mLog.warn("Unrecognized Link Control Message Size: {}", message.size()); } LCOpcode opcode = FullLCMessage.getOpcode(message); @@ -116,13 +125,29 @@ public class LCMessageFactory //Some Hytera Tier-3 systems use a zero-valued mask when the FLC is carried in the voice header or terminator //and in the same transmission it uses the standard mask when carried across the voice frames. It doesn't make //sense, but maybe it's a bug in their software implementation? - if(!valid && message.size() == 96 && opcode == LCOpcode.FULL_STANDARD_GROUP_VOICE_CHANNEL_USER) + if(residual != 0 && message.size() == 96 && opcode == LCOpcode.FULL_STANDARD_GROUP_VOICE_CHANNEL_USER) { //Retry the RS(12,9,4) with a mask value of zero - valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, 0); + residual = RS_12_9_DMR.correct(message, 0); } - FullLCMessage flc = null; + boolean valid = (residual != 0); + + if(!valid) + { + if(message.size() == 96) + { + //Check if the residual CRC check value is commonly seen for this opcode (ie RAS). + valid = mMaskManager.isValidRS12_9(opcode.getValue(), residual, timestamp); + } + else + { + //Check if the residual CRC check value is commonly seen for this opcode (ie RAS). + valid = mMaskManager.isValidCRC5(opcode.getValue(), residual, timestamp); + } + } + + FullLCMessage flc; switch(opcode) { @@ -208,47 +233,24 @@ public class LCMessageFactory { LCOpcode opcode = ShortLCMessage.getOpcode(message); - ShortLCMessage slc = null; - - switch(opcode) + ShortLCMessage slc = switch(opcode) { - case SHORT_STANDARD_NULL_MESSAGE: - slc = new NullMessage(message, timestamp, timeslot); - break; - case SHORT_STANDARD_ACTIVITY_UPDATE:; - slc = new ActivityUpdateMessage(message, timestamp, timeslot); - break; - case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION: - slc = new CapacityPlusRestChannel(message, timestamp, timeslot); - break; - case SHORT_CONNECT_PLUS_CONTROL_CHANNEL: - slc = new ConnectPlusControlChannel(message, timestamp, timeslot); - break; - case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL: - slc = new ConnectPlusTrafficChannel(message, timestamp, timeslot); - break; - case SHORT_STANDARD_CONTROL_CHANNEL_SYSTEM_PARAMETERS: - slc = new ControlChannelSystemParameters(message, timestamp, timeslot); - break; - case SHORT_STANDARD_TRAFFIC_CHANNEL_SYSTEM_PARAMETERS: - slc = new TrafficChannelSystemParameters(message, timestamp, timeslot); - break; - case SHORT_HYTERA_XPT_CHANNEL: - case SHORT_STANDARD_XPT_CHANNEL: - slc = new HyteraXPTChannel(message, timestamp, timeslot); - break; - case SHORT_STANDARD_UNKNOWN: - default: - slc = new UnknownShortLCMessage(message, timestamp, timeslot); - break; - } - - boolean valid = CRCDMR.crc8(message, 36) == 0; - -// System.out.println("SLC MSG:" + message.toHexString() + " CRC-8 RESIDUAL: " + CRCDMR.crc8(message, 36)); -// - slc.setValid(valid); + case SHORT_STANDARD_NULL_MESSAGE -> new NullMessage(message, timestamp, timeslot); + case SHORT_STANDARD_ACTIVITY_UPDATE -> new ActivityUpdateMessage(message, timestamp, timeslot); + case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION -> + new CapacityPlusRestChannel(message, timestamp, timeslot); + case SHORT_CONNECT_PLUS_CONTROL_CHANNEL -> new ConnectPlusControlChannel(message, timestamp, timeslot); + case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL -> new ConnectPlusTrafficChannel(message, timestamp, timeslot); + case SHORT_STANDARD_CONTROL_CHANNEL_SYSTEM_PARAMETERS -> + new ControlChannelSystemParameters(message, timestamp, timeslot); + case SHORT_STANDARD_TRAFFIC_CHANNEL_SYSTEM_PARAMETERS -> + new TrafficChannelSystemParameters(message, timestamp, timeslot); + case SHORT_HYTERA_XPT_CHANNEL, SHORT_STANDARD_XPT_CHANNEL -> + new HyteraXPTChannel(message, timestamp, timeslot); + default -> new UnknownShortLCMessage(message, timestamp, timeslot); + }; + slc.setValid(CRCDMR.crc8(message, 36) == 0); return slc; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java index c9921c82..94debd05 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java @@ -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 @@ -22,30 +22,30 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.BitSetFullException; import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.dmr.DMRCrcMaskManager; import io.github.dsheirer.module.decode.dmr.bptc.BPTC_128_77; import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory; import io.github.dsheirer.module.decode.dmr.message.type.LCSS; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Reassembles embedded signalling fragments into Full Link Control Messages */ public class FLCAssembler { - private final static Logger mLog = LoggerFactory.getLogger(FLCAssembler.class); private CorrectedBinaryMessage mAssemblingMessage; private int mFragmentCount = 0; private long mTimestamp; - private int mTimeslot; - private BPTC_128_77 mBPTC = new BPTC_128_77(); + private final int mTimeslot; + private final BPTC_128_77 mBPTC = new BPTC_128_77(); + private final LCMessageFactory mMessageFactory; /** * Constructs an instance */ - public FLCAssembler(int timeslot) + public FLCAssembler(int timeslot, DMRCrcMaskManager maskManager) { mTimeslot = timeslot; + mMessageFactory = new LCMessageFactory(maskManager); } /** @@ -111,8 +111,7 @@ public class FLCAssembler if(mFragmentCount == 4) { -// message = decode(mAssemblingMessage, mTimestamp, mTimeslot); - message = decode2(mAssemblingMessage, mTimestamp, mTimeslot); + message = decode(mAssemblingMessage, mTimestamp, mTimeslot); } mAssemblingMessage = null; @@ -123,12 +122,12 @@ public class FLCAssembler /** * Decodes an assembled FLC message, performs error detection and correction and returns a constructed FLC message */ - private FullLCMessage decode2(BinaryMessage message, long timestamp, int timeslot) + private FullLCMessage decode(BinaryMessage message, long timestamp, int timeslot) { if(message != null) { CorrectedBinaryMessage extractedMessage = mBPTC.extract(message); - FullLCMessage flco = LCMessageFactory.createFull(extractedMessage, timestamp, timeslot, false); + FullLCMessage flco = mMessageFactory.createFull(extractedMessage, timestamp, timeslot, false); if(extractedMessage.getCorrectedBitCount() < 0) { @@ -141,111 +140,6 @@ public class FLCAssembler return null; } - -// /** -// * Decodes an assembled FLC message, performs error detection and correction and returns a constructed FLC message -// */ -// private static FullLCMessage decode(BinaryMessage message, long timestamp, int timeslot) -// { -// if(message != null) -// { -// System.out.println("FLC Raw:" + message); -// boolean valid = true; -// -// CorrectedBinaryMessage descrambled = new CorrectedBinaryMessage(128); -// int src; -// descrambled.set(127, message.get(127)); -// for(int i = 0; i < 127; i++) -// { -// src = (i * 8) % 127; -// boolean x = message.get(src); -// descrambled.set(i, message.get(src)); -// } -// -// System.out.println("FLC Des:" + descrambled); -// int errorCount = 0; -// -// for(int row = 0; row < 8; row++) -// { -// int errorIndex = Hamming16.getErrorIndex(descrambled, row * 16); -// BinaryMessage rowErrors = BPTC_128_77.getRowErrors(descrambled); -// BinaryMessage columnErrors = BPTC_128_77.getColumnErrors(descrambled); -// -// int rowErrorCount = Hamming16.checkAndCorrect(descrambled, row * 16); -// -// if(rowErrorCount == 0) -// { -// System.out.println("Row [" + row + "]"); -// } -// else -// { -// System.out.println("Row [" + row + "] Error Count: " + rowErrorCount + " Index:" + errorIndex + -// " Value: " + message.get(errorIndex) + -// " Row Errors: " + rowErrors + " Column Errors: " + columnErrors ); -// } -// -// errorCount += rowErrorCount; -// -// if(errorCount > 1) -// { -// valid = false; -// } -// } -// -// //If valid, check the column parity values -// if(valid) -// { -// //Check the column parity values -// for(int column = 0; column < 16; column++) -// { -// boolean parity = descrambled.get(column); -// -// for(int y = 1; y < 8; y++) -// { -// parity ^= descrambled.get(column + (y * 16)); -// } -// -// if(parity) -// { -// valid = false; -// } -// } -// } -// -// //Extract the message. Row 1 and 2 are 11 bits. Rows 3-7 are 10 bits. -// CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(77); -// int pointer = 0; -// -// for(int row = 0; row < 2; row++) -// { -// for(int column = 0; column < 11; column++) -// { -// extractedMessage.set(pointer++, descrambled.get(row * 16 + column)); -// } -// } -// -// for(int row = 2; row < 7; row++) -// { -// for(int column = 0; column < 10; column++) -// { -// extractedMessage.set(pointer++, descrambled.get(row * 16 + column)); -// } -// -// //Transfer the checksum parity bit from this row -// extractedMessage.set(70 + row, descrambled.get(row * 16 + 10)); -// } -// -// extractedMessage.setCorrectedBitCount(errorCount); -// -// -// FullLCMessage flco = LCMessageFactory.createFull(extractedMessage, timestamp, timeslot, false); -// flco.setValid(valid); -// return flco; -// } -// -// return null; -// } - /** * Adds the cach payload to the currently assembling message */ diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java index a8aa0333..92b33365 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/terminator/Terminator.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 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 @@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl; import io.github.dsheirer.module.decode.dmr.message.data.SlotType; +import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern; /** @@ -39,10 +40,11 @@ public class Terminator extends DataMessageWithLinkControl * @param slotType for this data message * @param timestamp message was received * @param timeslot for the DMR burst + * @param linkControl message from the payload */ public Terminator(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, - long timestamp, int timeslot) + long timestamp, int timeslot, LCMessage linkControl) { - super(syncPattern, message, cach, slotType, timestamp, timeslot); + super(syncPattern, message, cach, slotType, timestamp, timeslot, linkControl); } } diff --git a/src/main/java/io/github/dsheirer/util/ByteUtil.java b/src/main/java/io/github/dsheirer/util/ByteUtil.java index 3b630f38..382b7619 100644 --- a/src/main/java/io/github/dsheirer/util/ByteUtil.java +++ b/src/main/java/io/github/dsheirer/util/ByteUtil.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -62,4 +62,16 @@ public class ByteUtil return sb.toString(); } + + public static String toHexString(int[] intBytes) + { + byte[] converted = new byte[intBytes.length]; + + for(int x = 0; x < intBytes.length; x++) + { + converted[x] = (byte)(intBytes[x] & 0xFF); + } + + return toHexString(converted); + } }