valueSet)
+ {
+ int distance = 40;
+
+ for(Long candidate: valueSet)
+ {
+ if(candidate != value)
+ {
+ long syndrome = candidate ^ value;
+ int candidateDistance = Long.bitCount(syndrome);
+ if(candidateDistance < distance)
+ {
+ distance = candidateDistance;
+ }
+ }
+ }
+
+ return distance;
+ }
+
+ /**
+ * Creates a codeword from the binary message where each set bit in the binary message corresponds to one of the
+ * words in the generator and we XOR the words together.
+ * @param message for generating a codeword
+ * @return codeword
+ */
+ public static long getCodeWord(BinaryMessage message)
+ {
+ long codeword = 0;
+
+ for(int x = 0; x < message.length(); x++)
+ {
+ if(message.get(x))
+ {
+ codeword ^= CHECKSUMS[x];
+ }
+ }
+
+ return codeword;
+ }
+
+ /**
+ * Calculates the checksum (ie codeword) from the set bits in the message.
+ * @param message with set bits, 0-8
+ * @return
+ */
+ private static long calculateChecksum(BinaryMessage message)
+ {
+ long calculated = 0; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(0); i < 9; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CHECKSUMS[i];
+ }
+
+ return calculated;
+ }
+
+// /**
+// * Performs error detection and returns a corrected copy of the 24-bit
+// * message that starts at the start index.
+// *
+// * @param message - source message containing startIndex + 24 bits length
+// * @return - corrected 24-bit galois value
+// */
+// public static int checkAndCorrect(CorrectedBinaryMessage message)
+// {
+// boolean parityError = message.cardinality() % 2 != 0;
+//
+// long syndrome = getSyndrome(message);
+//
+// /* No errors */
+// if(syndrome == 0)
+// {
+// if(parityError)
+// {
+// message.flip(23);
+// message.incrementCorrectedBitCount(1);
+// return 1;
+// }
+//
+// return 0;
+// }
+//
+// /* Get original message value */
+// int original = message.getInt(0, 22);
+//
+// int index = -1;
+// int syndromeWeight = 3;
+// int errors = 0;
+//
+// while(index < 23)
+// {
+// if(index != -1)
+// {
+// /* restore the previous flipped bit */
+// if(index > 0)
+// {
+// message.flip(index - 1);
+// }
+//
+// message.flip(index);
+//
+// syndromeWeight = 2;
+// }
+//
+// syndrome = getSyndrome(message);
+//
+// if(syndrome > 0)
+// {
+// for(int i = 0; i < 23; i++)
+// {
+//
+// errors = Long.bitCount(syndrome);
+//
+// if(errors <= syndromeWeight)
+// {
+// message.xor(12, 11, syndrome);
+//
+// message.rotateRight(i, 0, 22);
+//
+// if(index >= 0)
+// {
+// errors++;
+// }
+//
+// int corrected = message.getInt(0, 22);
+//
+// if(Integer.bitCount(original ^ corrected) > 3)
+// {
+// return 2;
+// }
+//
+// return 1;
+// }
+// else
+// {
+// message.rotateLeft(0, 22);
+// syndrome = getSyndrome(message);
+// }
+// }
+//
+// index++;
+// }
+// }
+//
+// return 2;
+// }
+
+ private static long getSyndrome(BinaryMessage message)
+ {
+ long calculated = calculateChecksum(message);
+ long checksum = message.getInt(12, 22);
+ return (checksum ^ calculated);
+ }
+
+ public static void main(String[] args)
+ {
+// Binary40_9_16 edac = new Binary40_9_16();
+
+ long mask = 0xFFFFFFFFFFl;
+ long sync = 0x575D57F7FFl;
+ long syncLeft = Long.rotateLeft(sync, 2) & mask;
+ long syncRight = Long.rotateRight(sync, 2) & mask;
+ System.out.println("Mask: " + Long.toBinaryString(sync));
+ System.out.println("Left: " + Long.toBinaryString(syncLeft));
+ System.out.println("Right: " + Long.toBinaryString(syncRight));
+
+ int distanceLeft = Long.bitCount(sync ^ syncLeft);
+ int distanceRight = Long.bitCount( sync ^ syncRight);
+
+ System.out.println("Left: " + distanceLeft);
+ System.out.println("Right: " + distanceRight);
+
+ System.out.println("Finished!");
+
+
+// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F3BB20"));
+// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F0C5C0"));
+// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("AFAC00"));
+//
+// System.out.println("M:" + bm.toHexString());
+// int a = Binary40_9_16.checkAndCorrect(bm);
+// System.out.println("M:" + bm.toHexString());
+//
+// System.out.println("A:" + a);
+
+
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/CRCP25.java b/src/main/java/io/github/dsheirer/edac/CRCP25.java
index a5b9c3895..35017c9d7 100644
--- a/src/main/java/io/github/dsheirer/edac/CRCP25.java
+++ b/src/main/java/io/github/dsheirer/edac/CRCP25.java
@@ -1,22 +1,26 @@
-/*******************************************************************************
- * sdr-trunk
- * Copyright (C) 2014-2018 Dennis Sheirer
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
- * later version.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
- * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along with this program.
- * If not, see
- *
- ******************************************************************************/
+ * 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.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -216,6 +220,63 @@ public class CRCP25
0x20000000l, 0x40000000l, 0x80000000l
};
+ /**
+ * CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 144-bit message and 12-bits of checksum
+ * using a polynomial of 0x1897 and generated by:
+ *
+ * long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
+ */
+ private static int[] CRC_12_FACCH = new int[]{0x256, 0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369,
+ 0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5, 0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB,
+ 0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4, 0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB,
+ 0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0, 0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1,
+ 0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4, 0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F,
+ 0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565, 0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49,
+ 0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA, 0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4,
+ 0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284, 0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5,
+ 0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F, 0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917,
+ 0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23, 0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897};
+
+ /**
+ * CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 168-bit message and 12-bits of checksum
+ * using a polynomial of 0x1897 and generated by:
+ *
+ * long[] checksums = CRCUtil.generate(168, 12, 0x1897l, 0x0l, false);
+ */
+ private static int[] CRC_12_SACCH = new int[]{0x47B, 0xE76, 0x73B, 0xFD6, 0x7EB, 0xFBE, 0x7DF, 0xFA4, 0x7D2, 0x3E9,
+ 0xDBF, 0xA94, 0x54A, 0x2A5, 0xD19, 0xAC7, 0x928, 0x494, 0x24A, 0x125, 0xCD9, 0xA27, 0x958, 0x4AC, 0x256,
+ 0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369, 0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5,
+ 0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB, 0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4,
+ 0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB, 0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0,
+ 0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1, 0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4,
+ 0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F, 0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565,
+ 0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49, 0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA,
+ 0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4, 0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284,
+ 0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5, 0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F,
+ 0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917, 0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23,
+ 0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897,};
+
+ /**
+ * CRC-16 checksums for P25 Phase 2 LCCH MAC PDU Contents. This is used for a 164-bit message and 16-bits of
+ * checksum using a polynomial of 0x11021 and generated by:
+ *
+ * long[] checksums = CRCUtil.generate(164, 16, 0x11021l, 0x0l, true);
+ */
+ private static int[] CRC_16_LCCH = new int[]{0xCF76, 0x67BB, 0xBBCD, 0xD5F6, 0x6AFB, 0xBD6D, 0xD6A6, 0x6B53, 0xBDB9,
+ 0xD6CC, 0x6B66, 0x35B3, 0x92C9, 0xC174, 0x60BA, 0x305D, 0x903E, 0x481F, 0xAC1F, 0xDE1F, 0xE71F, 0xFB9F,
+ 0xF5DF, 0xF2FF, 0xF16F, 0xF0A7, 0xF043, 0xF031, 0xF008, 0x7804, 0x3C02, 0x1E01, 0x8710, 0x4388, 0x21C4,
+ 0x10E2, 0x871, 0x8C28, 0x4614, 0x230A, 0x1185, 0x80D2, 0x4069, 0xA824, 0x5412, 0x2A09, 0x9D14, 0x4E8A,
+ 0x2745, 0x9BB2, 0x4DD9, 0xAEFC, 0x577E, 0x2BBF, 0x9DCF, 0xC6F7, 0xEB6B, 0xFDA5, 0xF6C2, 0x7B61, 0xB5A0,
+ 0x5AD0, 0x2D68, 0x16B4, 0xB5A, 0x5AD, 0x8AC6, 0x4563, 0xAAA1, 0xDD40, 0x6EA0, 0x3750, 0x1BA8, 0xDD4, 0x6EA,
+ 0x375, 0x89AA, 0x44D5, 0xAA7A, 0x553D, 0xA28E, 0x5147, 0xA0B3, 0xD849, 0xE434, 0x721A, 0x390D, 0x9496,
+ 0x4A4B, 0xAD35, 0xDE8A, 0x6F45, 0xBFB2, 0x5FD9, 0xA7FC, 0x53FE, 0x29FF, 0x9CEF, 0xC667, 0xEB23, 0xFD81,
+ 0xF6D0, 0x7B68, 0x3DB4, 0x1EDA, 0xF6D, 0x8FA6, 0x47D3, 0xABF9, 0xDDEC, 0x6EF6, 0x377B, 0x93AD, 0xC1C6,
+ 0x60E3, 0xB861, 0xD420, 0x6A10, 0x3508, 0x1A84, 0xD42, 0x6A1, 0x8B40, 0x45A0, 0x22D0, 0x1168, 0x8B4, 0x45A,
+ 0x22D, 0x8906, 0x4483, 0xAA51, 0xDD38, 0x6E9C, 0x374E, 0x1BA7, 0x85C3, 0xCAF1, 0xED68, 0x76B4, 0x3B5A,
+ 0x1DAD, 0x86C6, 0x4363, 0xA9A1, 0xDCC0, 0x6E60, 0x3730, 0x1B98, 0xDCC, 0x6E6, 0x373, 0x89A9, 0xCCC4, 0x6662,
+ 0x3331, 0x9188, 0x48C4, 0x2462, 0x1231, 0x8108, 0x4084, 0x2042, 0x1021, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20,
+ 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000};
+
/**
* Performs error detection and single-bit error correction against the
* data blocks of a PDU1 message.
@@ -402,11 +463,8 @@ else if(i > (messageStart + 15))
}
int checksum = message.getInt(messageStart + 7, messageStart + 15);
-
int residual = calculated ^ checksum;
-// mLog.debug( "CALC:" + calculated + " CHECK:" + checksum + " RESID:" + residual );
-
if(residual == 0 || residual == 0x1FF)
{
return CRC.PASSED;
@@ -441,12 +499,80 @@ public static boolean correctGalois24(CorrectedBinaryMessage tdulc)
return passes;
}
+ /**
+ * Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for S-OEMI/FACCH and compares it to the
+ * transmitted checksum to verify that the message is correct.
+ * Note: this uses the pre-calculated checksums in the CRC_12_FACCH array.
+ *
+ * @param message to verify
+ * @return true if the message passes the CRC-12 check.
+ */
+ public static boolean crc12_FACCH(CorrectedBinaryMessage message)
+ {
+ int calculated = 0xFFF; //Initial fill of all ones.
+
+ //Iterate the set bits and XOR the running checksum with pre-calculated lookup value
+ for(int i = message.nextSetBit(0); i >= 0 && i < 144; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CRC_12_FACCH[i];
+ }
+
+ int checksum = message.getInt(144, 155); //12-bit transmitted checksum
+ int residual = calculated ^ checksum;
+ return residual == 0;
+ }
+
+ /**
+ * Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for I-OEMI/SACCH and compares it to the
+ * transmitted checksum to verify that the message is correct.
+ * Note: this uses the pre-calculated checksums in the CRC_12_SACCH array.
+ *
+ * @param message to verify
+ * @return true if the message passes the CRC-12 check.
+ */
+ public static boolean crc12_SACCH(CorrectedBinaryMessage message)
+ {
+ int calculated = 0xFFF; //Initial fill of all ones.
+
+ //Iterate the set bits and XOR the running checksum with pre-calculated lookup value
+ for(int i = message.nextSetBit(0); i >= 0 && i < 168; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CRC_12_SACCH[i];
+ }
+
+ int checksum = message.getInt(168, 179); //12-bit transmitted checksum
+ int residual = calculated ^ checksum;
+ return residual == 0;
+ }
+
+ /**
+ * Calculates the CRC-16 checksum for the P25 Phase 2 MAC PDU Contents for I-OECI/LCCH and compares it to the
+ * transmitted checksum to verify that the message is correct.
+ * Note: this uses the pre-calculated checksums in the CRC_16_LCCH array.
+ *
+ * @param message to verify
+ * @return true if the message passes the CRC-16 check.
+ */
+ public static boolean crc16_LCCH(CorrectedBinaryMessage message)
+ {
+ int calculated = 0xFFFF; //Initial fill of all ones.
+
+ //Iterate the set bits and XOR the running checksum with pre-calculated lookup value
+ for(int i = message.nextSetBit(0); i >= 0 && i < 164; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CRC_16_LCCH[i];
+ }
+
+ int checksum = message.getInt(164, 179); //16-bit transmitted checksum
+ int residual = calculated ^ checksum;
+ return residual == 0;
+ }
+
/**
* Calculates the value of the message checksum as a long
*/
- public static long getLongChecksum(BinaryMessage message,
- int crcStart, int crcLength)
+ public static long getLongChecksum(BinaryMessage message, int crcStart, int crcLength)
{
return message.getLong(crcStart, crcStart + crcLength - 1);
}
@@ -454,8 +580,7 @@ public static long getLongChecksum(BinaryMessage message,
/**
* Calculates the value of the message checksum as an integer
*/
- public static int getIntChecksum(BinaryMessage message,
- int crcStart, int crcLength)
+ public static int getIntChecksum(BinaryMessage message, int crcStart, int crcLength)
{
return message.getInt(crcStart, crcStart + crcLength - 1);
}
@@ -494,16 +619,19 @@ public static int getBitError(int checksumError, int[] checksums)
public static void main(String[] args)
{
- String raw = "000000001000001100000001010001111011000100001010010001111100000000000101000000000000000001000000000000110000000000000001101010101010101010101010";
-
- BinaryMessage message = BinaryMessage.load(raw);
-
- mLog.debug("MSG:" + message.toString());
-
- CRC results = checkCRC9(message, 0);
-
- mLog.debug("COR:" + message.toString());
-
- mLog.debug("Results: " + results.getDisplayText());
+ long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
+ System.out.println("Checksums:" + Arrays.toString(checksums));
+
+ String hex12 = "7C0000000000000000000000000000000000CA7";
+
+// BinaryMessage message = BinaryMessage.load(raw);
+//
+// mLog.debug("MSG:" + message.toString());
+//
+// CRC results = checkCRC9(message, 0);
+//
+// mLog.debug("COR:" + message.toString());
+//
+// mLog.debug("Results: " + results.getDisplayText());
}
}
diff --git a/src/main/java/io/github/dsheirer/edac/CRCUtil.java b/src/main/java/io/github/dsheirer/edac/CRCUtil.java
index be7382df4..572c12181 100644
--- a/src/main/java/io/github/dsheirer/edac/CRCUtil.java
+++ b/src/main/java/io/github/dsheirer/edac/CRCUtil.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,8 +33,7 @@ public static long[] generate(int messageSize,
long initialFill,
boolean includeCRCBitErrors)
{
- long[] crcTable = new long[messageSize +
- (includeCRCBitErrors ? crcSize : 0)];
+ long[] crcTable = new long[messageSize + (includeCRCBitErrors ? crcSize : 0)];
int[] checksumIndexes = new int[crcSize];
@@ -132,20 +132,12 @@ public static Parity parity(long value)
* @return message with all message bits zeroed out, and the remainder
* placed in the crc field which starts at index messageLength
*/
- public static BinaryMessage decode(BinaryMessage message,
- int messageStart,
- int messageSize,
- long polynomial,
- int crcSize)
+ public static BinaryMessage decode(BinaryMessage message, int messageStart, int messageSize, long polynomial, int crcSize)
{
- for(int i = message.nextSetBit(messageStart);
- i >= messageStart && i < messageSize;
- i = message.nextSetBit(i + 1))
+ for(int i = message.nextSetBit(messageStart); i >= messageStart && i < messageSize; i = message.nextSetBit(i + 1))
{
BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
-
polySet.load(i, crcSize + 1, polynomial);
-
message.xor(polySet);
System.out.println(message.toString());
}
@@ -226,8 +218,11 @@ public static void main(String[] args)
{
mLog.debug("Starting");
- long poly = 0x13l;
- long[] checksums = generate(32, 4, poly, 0, true);
+ int messageSize = 164;
+ int crcSize = 16;
+ long poly = 0x11021l;
+ long initialFill = 0x0l;
+ long[] checksums = CRCUtil.generate(messageSize, crcSize, poly, initialFill, true);
StringBuilder sb = new StringBuilder();
sb.append("private static int[] CHECKSUMS = new int[]{");
@@ -238,7 +233,29 @@ public static void main(String[] args)
}
sb.append("};");
-
System.out.println("Checksums:\n" + sb);
+
+ String hex = "7CFC0039420170452A4F9970000000000000000000D05";
+ CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
+
+
+ int calculated = 0xFFF; //Initial fill of all ones
+
+ int messageStart = 0;
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(messageStart);
+ i >= messageStart && i < messageSize;
+ i = message.nextSetBit(i + 1))
+ {
+ System.out.println("Bit [" + i + "] is set");
+ calculated ^= checksums[i];
+ }
+
+ int checksum = message.getInt(messageSize, messageSize + crcSize - 1);
+ int residual = calculated ^ checksum;
+
+ mLog.debug("CALC:" + Integer.toHexString(calculated).toUpperCase() +
+ " CHECK:" + Integer.toHexString(checksum).toUpperCase() +
+ " RESIDUAL:" + Integer.toHexString(residual).toUpperCase());
}
}
diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
index b42cf76e7..b09b21ae6 100644
--- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
+++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,7 +35,7 @@
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
-import io.github.dsheirer.gui.viewer.RecordingViewer;
+import io.github.dsheirer.gui.viewer.MessageRecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
@@ -84,7 +84,7 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager;
private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor;
- private RecordingViewer mRecordingViewer;
+ private MessageRecordingViewer mMessageRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
@@ -216,14 +216,14 @@ public Stage getRecordingViewerStage()
return mRecordingViewerStage;
}
- public RecordingViewer getRecordingViewer()
+ public MessageRecordingViewer getRecordingViewer()
{
- if(mRecordingViewer == null)
+ if(mMessageRecordingViewer == null)
{
- mRecordingViewer = new RecordingViewer();
+ mMessageRecordingViewer = new MessageRecordingViewer();
}
- return mRecordingViewer;
+ return mMessageRecordingViewer;
}
public Stage getIconManagerStage()
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java
index d52615f42..28b3f5163 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -647,11 +647,13 @@ public class NewP25P2ChannelMenu extends Menu
public NewP25P2ChannelMenu()
{
setText(DecoderType.P25_PHASE2.getDisplayString());
- MenuItem trunked = new MenuItem("Trunked System");
- trunked.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
- MenuItem channel = new MenuItem("Individual Channel");
+ MenuItem trunkedP1 = new MenuItem("Trunked System - FDMA Phase 1 Control Channel");
+ trunkedP1.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
+ MenuItem trunkedP2 = new MenuItem("Trunked System - TDMA Phase 2 Control Channel");
+ trunkedP2.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
+ MenuItem channel = new MenuItem("Individual Phase 2 Channel");
channel.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
- getItems().addAll(trunked, channel);
+ getItems().addAll(trunkedP1, trunkedP2, channel);
}
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java
index adb045677..632185621 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P1ConfigurationEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -113,7 +113,7 @@ private TitledPane getDecoderPane()
if(mDecoderPane == null)
{
mDecoderPane = new TitledPane();
- mDecoderPane.setText("Decoder: P25 Phase 1 (or P25 Phase 2 Control Channel)");
+ mDecoderPane.setText("Decoder: P25 Phase 1 (also used for P25 Phase 2 system with FDMA control channels)");
mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
@@ -355,9 +355,9 @@ protected void saveDecoderConfiguration()
{
DecodeConfigP25Phase1 config;
- if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1)
+ if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1)
{
- config = (DecodeConfigP25Phase1)getItem().getDecodeConfiguration();
+ config = p1;
}
else
{
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P2ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P2ConfigurationEditor.java
index a9a2759ad..1201b461a 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P2ConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/P25P2ConfigurationEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,9 +42,13 @@
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TitledPane;
+import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
+import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +68,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
private IntegerTextField mWacnTextField;
private IntegerTextField mSystemTextField;
private IntegerTextField mNacTextField;
+ private ToggleSwitch mIgnoreDataCallsButton;
+ private Spinner mTrafficChannelPoolSizeSpinner;
/**
* Constructs an instance
@@ -102,7 +108,7 @@ private TitledPane getDecoderPane()
if(mDecoderPane == null)
{
mDecoderPane = new TitledPane();
- mDecoderPane.setText("Decoder: P25 Phase 2");
+ mDecoderPane.setText("Decoder: P25 Phase 2 for TDMA control or TDMA traffic channels.");
mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
@@ -112,6 +118,22 @@ private TitledPane getDecoderPane()
int row = 0;
+ Label poolSizeLabel = new Label("Max Traffic Channels");
+ GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
+ GridPane.setConstraints(poolSizeLabel, 0, row);
+ gridPane.getChildren().add(poolSizeLabel);
+
+ GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row);
+ gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
+
+ GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row);
+ gridPane.getChildren().add(getIgnoreDataCallsButton());
+
+ Label directionLabel = new Label("Ignore Data Calls");
+ GridPane.setHalignment(directionLabel, HPos.LEFT);
+ GridPane.setConstraints(directionLabel, 3, row);
+ gridPane.getChildren().add(directionLabel);
+
Label wacnLabel = new Label("WACN");
GridPane.setHalignment(wacnLabel, HPos.RIGHT);
GridPane.setConstraints(wacnLabel, 0, ++row);
@@ -122,20 +144,27 @@ private TitledPane getDecoderPane()
Label systemLabel = new Label("System");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
- GridPane.setConstraints(systemLabel, 0, ++row);
+ GridPane.setConstraints(systemLabel, 2, row);
gridPane.getChildren().add(systemLabel);
- GridPane.setConstraints(getSystemTextField(), 1, row);
+ GridPane.setConstraints(getSystemTextField(), 3, row);
gridPane.getChildren().add(getSystemTextField());
Label nacLabel = new Label("NAC");
GridPane.setHalignment(nacLabel, HPos.RIGHT);
- GridPane.setConstraints(nacLabel, 0, ++row);
+ GridPane.setConstraints(nacLabel, 4, row);
gridPane.getChildren().add(nacLabel);
- GridPane.setConstraints(getNacTextField(), 1, row);
+ GridPane.setConstraints(getNacTextField(), 5, row);
gridPane.getChildren().add(getNacTextField());
+ Label noteLabel = new Label("Note: WACN/System/NAC values are auto-detected (ie not required) from " +
+ "the control channel and are only required when decoding individual traffic channels");
+ GridPane.setHalignment(noteLabel, HPos.LEFT);
+ GridPane.setConstraints(noteLabel, 1, ++row, 6, 1);
+ gridPane.getChildren().add(noteLabel);
+
+
mDecoderPane.setContent(gridPane);
}
@@ -203,6 +232,37 @@ private EventLogConfigurationEditor getEventLogConfigurationEditor()
return mEventLogConfigurationEditor;
}
+ private ToggleSwitch getIgnoreDataCallsButton()
+ {
+ if(mIgnoreDataCallsButton == null)
+ {
+ mIgnoreDataCallsButton = new ToggleSwitch();
+ mIgnoreDataCallsButton.setDisable(true);
+ mIgnoreDataCallsButton.selectedProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mIgnoreDataCallsButton;
+ }
+
+ private Spinner getTrafficChannelPoolSizeSpinner()
+ {
+ if(mTrafficChannelPoolSizeSpinner == null)
+ {
+ mTrafficChannelPoolSizeSpinner = new Spinner();
+ mTrafficChannelPoolSizeSpinner.setDisable(true);
+ mTrafficChannelPoolSizeSpinner.setTooltip(
+ new Tooltip("Maximum number of traffic channels that can be created by the decoder"));
+ mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+ SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50);
+ mTrafficChannelPoolSizeSpinner.setValueFactory(svf);
+ mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mTrafficChannelPoolSizeSpinner;
+ }
+
private IntegerTextField getWacnTextField()
{
if(mWacnTextField == null)
@@ -247,6 +307,9 @@ private RecordConfigurationEditor getRecordConfigurationEditor()
types.add(RecorderType.BASEBAND);
types.add(RecorderType.DEMODULATED_BIT_STREAM);
types.add(RecorderType.MBE_CALL_SEQUENCE);
+ types.add(RecorderType.TRAFFIC_BASEBAND);
+ types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM);
+ types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE);
mRecordConfigurationEditor = new RecordConfigurationEditor(types);
mRecordConfigurationEditor.setDisable(true);
mRecordConfigurationEditor.modifiedProperty()
@@ -259,9 +322,8 @@ private RecordConfigurationEditor getRecordConfigurationEditor()
@Override
protected void setDecoderConfiguration(DecodeConfiguration config)
{
- if(config instanceof DecodeConfigP25Phase2)
+ if(config instanceof DecodeConfigP25Phase2 decodeConfig)
{
- DecodeConfigP25Phase2 decodeConfig = (DecodeConfigP25Phase2)config;
getWacnTextField().setDisable(false);
getSystemTextField().setDisable(false);
getNacTextField().setDisable(false);
@@ -280,6 +342,11 @@ protected void setDecoderConfiguration(DecodeConfiguration config)
getSystemTextField().set(0);
getNacTextField().set(0);
}
+
+ getIgnoreDataCallsButton().setDisable(false);
+ getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
+ getTrafficChannelPoolSizeSpinner().setDisable(false);
+ getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
}
else
{
@@ -289,6 +356,8 @@ protected void setDecoderConfiguration(DecodeConfiguration config)
getWacnTextField().setDisable(true);
getSystemTextField().setDisable(true);
getNacTextField().setDisable(true);
+ getIgnoreDataCallsButton().setDisable(true);
+ getTrafficChannelPoolSizeSpinner().setDisable(true);
}
}
@@ -297,20 +366,23 @@ protected void saveDecoderConfiguration()
{
DecodeConfigP25Phase2 config;
- if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2)
+ if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2 p2)
{
- config = (DecodeConfigP25Phase2)getItem().getDecodeConfiguration();
- config.setAutoDetectScrambleParameters(false);
- int wacn = getWacnTextField().get();
- int system = getSystemTextField().get();
- int nac = getNacTextField().get();
- config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
+ config = p2;
}
else
{
config = new DecodeConfigP25Phase2();
}
+ config.setAutoDetectScrambleParameters(false);
+ int wacn = getWacnTextField().get();
+ int system = getSystemTextField().get();
+ int nac = getNacTextField().get();
+ config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
+ config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
+ config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
+
getItem().setDecodeConfiguration(config);
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/EnrichedSite.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/EnrichedSite.java
index 659c65c88..163339b01 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/EnrichedSite.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/EnrichedSite.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,13 +25,15 @@
/**
* Wrapper class to join a Site and a corresponding County Info
*/
-public class EnrichedSite
+public class EnrichedSite implements Comparable
{
+ private static final String PHASE_2_TDMA_MODULATION = "TDMA";
private Site mSite;
private CountyInfo mCountyInfo;
/**
* Constructs an instance
+ *
* @param site object
* @param countyInfo optional
*/
@@ -41,8 +43,16 @@ public EnrichedSite(Site site, CountyInfo countyInfo)
mCountyInfo = countyInfo;
}
+ public static String format(int value)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(value).append(" (").append(Integer.toHexString(value).toUpperCase()).append(")");
+ return sb.toString();
+ }
+
/**
* Site instance
+ *
* @return site or null
*/
public Site getSite()
@@ -52,6 +62,7 @@ public Site getSite()
/**
* Sets the site instance
+ *
* @param site or null
*/
public void setSite(Site site)
@@ -61,6 +72,7 @@ public void setSite(Site site)
/**
* County information
+ *
* @return county info or null
*/
public CountyInfo getCountyInfo()
@@ -70,6 +82,7 @@ public CountyInfo getCountyInfo()
/**
* Sets the county info
+ *
* @param countyInfo or null
*/
public void setCountyInfo(CountyInfo countyInfo)
@@ -78,13 +91,26 @@ public void setCountyInfo(CountyInfo countyInfo)
}
/**
- * Optional site number
+ * Formatted system identity
+ *
+ * @return
*/
- public Integer getSiteNumber()
+ public String getSystemFormatted()
{
if(mSite != null)
{
- return mSite.getSiteNumber();
+ //System number is stored in the zone number field.
+ return format(mSite.getZoneNumber());
+ }
+
+ return null;
+ }
+
+ public String getSiteFormatted()
+ {
+ if(mSite != null)
+ {
+ return format(mSite.getSiteNumber());
}
return null;
@@ -93,11 +119,11 @@ public Integer getSiteNumber()
/**
* Optional site RFSS value
*/
- public Integer getRfss()
+ public String getRfssFormatted()
{
if(mSite != null)
{
- return mSite.getRfss();
+ return format(mSite.getRfss());
}
return null;
@@ -128,4 +154,23 @@ public String getDescription()
return null;
}
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ String system = getSystemFormatted();
+ sb.append(system == null ? "-" : system).append(" ");
+ String rfss = getRfssFormatted();
+ sb.append(rfss == null ? "-" : rfss).append(" ");
+ String site = getSiteFormatted();
+ sb.append(site == null ? "-" : site);
+ return sb.toString();
+ }
+
+ @Override
+ public int compareTo(EnrichedSite o)
+ {
+ return this.toString().compareTo(o.toString());
+ }
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
index d0eb7b95f..8ef9655be 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -449,6 +449,16 @@ else if(flavor.getName().contentEquals("Passport"))
* @return decoder type or null.
*/
public DecoderType getDecoderType(System system)
+ {
+ return getDecoderType(system, null);
+ }
+
+ /**
+ * Decoder type for the specified system, if supported.
+ * @param system requiring a decoder type
+ * @return decoder type or null.
+ */
+ public DecoderType getDecoderType(System system, Site site)
{
Type type = getType(system);
Flavor flavor = getFlavor(system);
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
index 498d4bc9d..41890b68b 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,6 +35,7 @@
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.rrapi.type.Flavor;
import io.github.dsheirer.rrapi.type.RadioNetwork;
import io.github.dsheirer.rrapi.type.Site;
import io.github.dsheirer.rrapi.type.SiteFrequency;
@@ -42,6 +43,13 @@
import io.github.dsheirer.rrapi.type.SystemInformation;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
import javafx.animation.RotateTransition;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
@@ -74,14 +82,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Predicate;
-
public class SiteEditor extends GridPane
{
private static final Logger mLog = LoggerFactory.getLogger(SiteEditor.class);
@@ -90,6 +90,8 @@ public class SiteEditor extends GridPane
private static final String PRIMARY_CONTROL_CHANNEL = "d";
private static final String TOGGLE_BUTTON_CONTROL = "Control";
private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice";
+ private static final String PHASE_2_TDMA_MODULATION = "TDMA";
+ private static final String PHASE_2_FLAVOR = "Phase II";
private UserPreferences mUserPreferences;
private PlaylistManager mPlaylistManager;
@@ -115,6 +117,10 @@ public class SiteEditor extends GridPane
private SystemInformation mCurrentSystemInformation;
private ComboBox mAliasListNameComboBox;
private Button mNewAliasListButton;
+ private Label mP25ControlLabel;
+ private SegmentedButton mP25ControlSegmentedButton;
+ private ToggleButton mFdmaControlToggleButton;
+ private ToggleButton mTdmaControlToggleButton;
public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManager)
{
@@ -163,6 +169,13 @@ public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManag
GridPane.setConstraints(getConfigurationsSegmentedButton(), 2, row);
getChildren().add(getConfigurationsSegmentedButton());
+ GridPane.setConstraints(getP25ControlLabel(), 1, ++row);
+ GridPane.setHalignment(getP25ControlLabel(), HPos.RIGHT);
+ getChildren().add(getP25ControlLabel());
+
+ GridPane.setConstraints(getP25ControlSegmentedButton(), 2, row);
+ getChildren().add(getP25ControlSegmentedButton());
+
Label systemLabel = new Label("System");
GridPane.setConstraints(systemLabel, 1, ++row);
GridPane.setHalignment(systemLabel, HPos.RIGHT);
@@ -206,7 +219,7 @@ public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManag
createBox.setAlignment(Pos.CENTER_LEFT);
createBox.setSpacing(10);
createBox.getChildren().addAll(getCreateChannelConfigurationButton(), getGoToChannelEditorCheckBox());
- GridPane.setConstraints(createBox, 2, ++row + 1);
+ GridPane.setConstraints(createBox, 2, ++row);
getChildren().addAll(createBox);
//Note: the following label node is added to the same location as the buttons and visibility is toggled
@@ -215,6 +228,13 @@ public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManag
getChildren().add(getProtocolNotSupportedLabel());
}
+ /**
+ * Creates a channel decode configuration for the specified site.
+ * @param decoderType to create
+ * @param site information and frequencies
+ * @param systemInformation with frequency mapping
+ * @return decode configuration
+ */
private DecodeConfiguration getDecodeConfiguration(DecoderType decoderType, Site site, SystemInformation systemInformation)
{
if(decoderType == null)
@@ -298,7 +318,13 @@ private DecodeConfiguration getDecodeConfiguration(DecoderType decoderType, Site
}
}
-
+ /**
+ * Loads the user selected site info into this editor.
+ * @param site to load
+ * @param system for the site
+ * @param systemInformation for the site
+ * @param decoder to lookup supplemental information
+ */
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
{
mCurrentSite = site;
@@ -346,6 +372,29 @@ public void setSite(EnrichedSite site, System system, SystemInformation systemIn
}
else
{
+ Flavor flavor = decoder.getFlavor(system);
+
+ if(flavor != null && flavor.getName() != null && flavor.getName().equals(PHASE_2_FLAVOR))
+ {
+ getP25ControlLabel().setVisible(true);
+ getTdmaControlToggleButton().setVisible(true);
+ getFdmaControlToggleButton().setVisible(true);
+ if(site.getSite().getModulation() != null && site.getSite().getModulation().contains(PHASE_2_TDMA_MODULATION))
+ {
+ getTdmaControlToggleButton().setSelected(true);
+ }
+ else
+ {
+ getFdmaControlToggleButton().setSelected(true);
+ }
+ }
+ else
+ {
+ getP25ControlLabel().setVisible(false);
+ getTdmaControlToggleButton().setVisible(false);
+ getFdmaControlToggleButton().setVisible(false);
+ }
+
getCreateChannelConfigurationButton().setVisible(true);
getGoToChannelEditorCheckBox().setVisible(true);
getSystemTextField().setText(system.getName());
@@ -514,10 +563,13 @@ private void createControlChannel()
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
- //Change a phase 2 system to use the phase 1 control channel
+ //Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
if(decoderType == DecoderType.P25_PHASE2)
{
- decoderType = DecoderType.P25_PHASE1;
+ if(getFdmaControlToggleButton().isSelected())
+ {
+ decoderType = DecoderType.P25_PHASE1;
+ }
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
@@ -587,10 +639,13 @@ private void createControlAndAlternatesChannel()
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
- //Change a phase 2 system to use the phase 1 control channel
+ //Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
if(decoderType == DecoderType.P25_PHASE2)
{
- decoderType = DecoderType.P25_PHASE1;
+ if(getFdmaControlToggleButton().isSelected())
+ {
+ decoderType = DecoderType.P25_PHASE1;
+ }
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
@@ -685,6 +740,16 @@ private void createChannels(boolean selectedOnly)
Channel channel = getChannelTemplate();
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
+
+ //Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
+ if(decoderType == DecoderType.P25_PHASE2)
+ {
+ if(getFdmaControlToggleButton().isSelected())
+ {
+ decoderType = DecoderType.P25_PHASE1;
+ }
+ }
+
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
@@ -750,6 +815,70 @@ private static long getFrequency(SiteFrequency siteFrequency)
return (long)(siteFrequency.getFrequency() * 1E6);
}
+ /**
+ * P25 Phase 2 control channel label.
+ */
+ private Label getP25ControlLabel()
+ {
+ if(mP25ControlLabel == null)
+ {
+ mP25ControlLabel = new Label("Control");
+ mP25ControlLabel.setVisible(false);
+ }
+
+ return mP25ControlLabel;
+ }
+
+ /**
+ * P25 Phase 2 control channel type (TDMA vs FDMA) selection buttons.
+ */
+ private SegmentedButton getP25ControlSegmentedButton()
+ {
+ if(mP25ControlSegmentedButton == null)
+ {
+ mP25ControlSegmentedButton = new SegmentedButton(getFdmaControlToggleButton(), getTdmaControlToggleButton());
+ mP25ControlSegmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+ mP25ControlSegmentedButton.getToggleGroup().selectedToggleProperty()
+ .addListener((observable, oldValue, newValue) -> {
+ //Ensure that one button is always selected
+ if(newValue == null)
+ {
+ oldValue.setSelected(true);
+ }
+ });
+ }
+
+ return mP25ControlSegmentedButton;
+ }
+
+ /**
+ * P25 Phase 2 with FDMA (phase 1) control channel selection button.
+ */
+ private ToggleButton getFdmaControlToggleButton()
+ {
+ if(mFdmaControlToggleButton == null)
+ {
+ mFdmaControlToggleButton = new ToggleButton("FDMA Phase 1");
+ mFdmaControlToggleButton.setVisible(false);
+ }
+
+ return mFdmaControlToggleButton;
+ }
+
+ /**
+ * P25 Phase 2 with TDMA (phase 2) control channel selection button.
+ */
+ private ToggleButton getTdmaControlToggleButton()
+ {
+ if(mTdmaControlToggleButton == null)
+ {
+ mTdmaControlToggleButton = new ToggleButton("TDMA Phase 2");
+ mTdmaControlToggleButton.setVisible(false);
+ }
+
+ return mTdmaControlToggleButton;
+ }
+
/**
* Flashes the alias list combobox to let the user know that they must select an alias list
*/
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemSiteSelectionEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemSiteSelectionEditor.java
index a9bac122e..8be96c12d 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemSiteSelectionEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemSiteSelectionEditor.java
@@ -22,6 +22,7 @@
package io.github.dsheirer.gui.playlist.radioreference;
+import com.google.common.collect.Ordering;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Flavor;
@@ -42,6 +43,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
@@ -126,6 +129,7 @@ public void setSystem(System system, List sites, RadioReferenceDec
getFlavorLabel().setText(flavor != null ? flavor.getName() : null);
Voice voice = decoder.getVoice(system);
getVoiceLabel().setText(voice != null ? voice.getName() : null);
+ Collections.sort(sites, Ordering.natural());
getSiteTableView().getItems().addAll(sites);
setLoading(false);
}
@@ -212,27 +216,33 @@ private TableView getSiteTableView()
mSiteTableView = new TableView<>();
mSiteTableView.setPlaceholder(getPlaceholderLabel());
- TableColumn numberColumn = new TableColumn();
- numberColumn.setText("Site");
- numberColumn.setCellValueFactory(new PropertyValueFactory<>("siteNumber"));
- numberColumn.setPrefWidth(75);
+ TableColumn systemColumn = new TableColumn();
+ systemColumn.setText("System");
+ systemColumn.setCellValueFactory(new PropertyValueFactory<>("systemFormatted"));
+ systemColumn.setPrefWidth(60);
TableColumn rfssColumn = new TableColumn();
rfssColumn.setText("RFSS");
- rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfss"));
+ rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfssFormatted"));
rfssColumn.setPrefWidth(60);
- TableColumn countyColumn = new TableColumn();
- countyColumn.setText("County");
- countyColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
- countyColumn.setPrefWidth(125);
+ TableColumn siteColumn = new TableColumn();
+ siteColumn.setText("Site");
+ siteColumn.setCellValueFactory(new PropertyValueFactory<>("siteFormatted"));
+ siteColumn.setPrefWidth(75);
+
+ TableColumn countyNameColumn = new TableColumn();
+ countyNameColumn.setText("County");
+ countyNameColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
+ countyNameColumn.setPrefWidth(125);
TableColumn descriptionColumn = new TableColumn();
descriptionColumn.setText("Name");
descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description"));
descriptionColumn.setPrefWidth(400);
- mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
+ mSiteTableView.getColumns().addAll(systemColumn, rfssColumn, siteColumn, countyNameColumn,
+ descriptionColumn);
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mSiteTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, selected) ->
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/ChannelStartProcessingRequestViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/ChannelStartProcessingRequestViewer.java
new file mode 100644
index 000000000..b97f61052
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/ChannelStartProcessingRequestViewer.java
@@ -0,0 +1,234 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import javafx.geometry.HPos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+
+/**
+ * Viewer for channel start processing requests
+ */
+public class ChannelStartProcessingRequestViewer extends HBox
+{
+ private Label mChannelConfigLabel;
+ private Label mChannelDescriptorLabel;
+ private Label mTrafficChannelManagerLabel;
+ private Label mPreloadDataContentLabel;
+ private Label mParentDecodeHistoryLabel;
+ private Label mChildDecodeHistoryLabel;
+ private IdentifierCollectionViewer mIdentifierCollectionViewer;
+
+ /**
+ * Constructs an instance
+ */
+ public ChannelStartProcessingRequestViewer()
+ {
+ GridPane gridPane = new GridPane();
+ gridPane.setVgap(5);
+ gridPane.setHgap(5);
+
+ gridPane.add(new Label("Configuration:"), 0, 0);
+ GridPane.setHgrow(getChannelConfigLabel(), Priority.ALWAYS);
+ gridPane.add(getChannelConfigLabel(), 1, 0);
+
+ gridPane.add(new Label("Channel:"), 0, 1);
+ GridPane.setHgrow(getChannelDescriptorLabel(), Priority.ALWAYS);
+ gridPane.add(getChannelDescriptorLabel(), 1, 1);
+
+ gridPane.add(new Label("TCM:"), 0, 2);
+ GridPane.setHgrow(getTrafficChannelManagerLabel(), Priority.ALWAYS);
+ gridPane.add(getTrafficChannelManagerLabel(), 1, 2);
+
+ gridPane.add(new Label("Preload Items:"), 0, 3);
+ GridPane.setHgrow(getPreloadDataContentLabel(), Priority.ALWAYS);
+ gridPane.add(getPreloadDataContentLabel(), 1, 3);
+
+ gridPane.add(new Label("Parent History:"), 0, 4);
+ GridPane.setHgrow(getParentDecodeHistoryLabel(), Priority.ALWAYS);
+ gridPane.add(getParentDecodeHistoryLabel(), 1, 4);
+
+ gridPane.add(new Label("Child History:"), 0, 5);
+ GridPane.setHgrow(getChildDecodeHistoryLabel(), Priority.ALWAYS);
+ gridPane.add(getChildDecodeHistoryLabel(), 1, 5);
+
+ gridPane.add(new Label("Identifiers"), 0, 6);
+ GridPane.setHgrow(getIdentifierCollectionViewer(), Priority.ALWAYS);
+ getIdentifierCollectionViewer().setPrefHeight(120);
+ gridPane.add(getIdentifierCollectionViewer(), 0, 7, 2, 1);
+
+ ColumnConstraints cc0 = new ColumnConstraints();
+ cc0.setHalignment(HPos.RIGHT);
+ gridPane.getColumnConstraints().add(cc0);
+
+ getChildren().add(gridPane);
+ }
+
+ public void set(ChannelStartProcessingRequest request)
+ {
+ if(request != null)
+ {
+ if(request.getChannel() != null)
+ {
+ getChannelConfigLabel().setText(request.getChannel().toString());
+ }
+ else
+ {
+ getChannelConfigLabel().setText("None");
+ }
+
+ if(request.getChannelDescriptor() != null)
+ {
+ getChannelDescriptorLabel().setText(request.getChannelDescriptor().toString() + " " +
+ request.getChannelDescriptor().getDownlinkFrequency() + " MHz");
+ }
+ else
+ {
+ getChannelDescriptorLabel().setText("None");
+ }
+
+ if(request.getChildDecodeEventHistory() != null)
+ {
+ getChildDecodeHistoryLabel().setText(request.getChildDecodeEventHistory().getItems().size() + " items");
+ }
+ else
+ {
+ getChildDecodeHistoryLabel().setText("None");
+ }
+
+ if(request.getParentDecodeEventHistory() != null)
+ {
+ getParentDecodeHistoryLabel().setText(request.getParentDecodeEventHistory().getItems().size() + " items");
+ }
+ else
+ {
+ getParentDecodeHistoryLabel().setText("None");
+ }
+
+ if(request.getPreloadDataContents() != null)
+ {
+ getPreloadDataContentLabel().setText(request.getPreloadDataContents().size() + " items");
+ }
+ else
+ {
+ getPreloadDataContentLabel().setText("None");
+ }
+
+ if(request.getTrafficChannelManager() != null)
+ {
+ getTrafficChannelManagerLabel().setText(request.getTrafficChannelManager().getClass().getName());
+ }
+ else
+ {
+ getTrafficChannelManagerLabel().setText("None:");
+ }
+
+ if(request.getIdentifierCollection() != null)
+ {
+ getIdentifierCollectionViewer().set(request.getIdentifierCollection());
+ }
+ }
+ else
+ {
+ getChannelDescriptorLabel().setText(null);
+ getChildDecodeHistoryLabel().setText(null);
+ getChannelConfigLabel().setText(null);
+ getIdentifierCollectionViewer().set(null);
+ getParentDecodeHistoryLabel().setText(null);
+ getPreloadDataContentLabel().setText(null);
+ getTrafficChannelManagerLabel().setText(null);
+ }
+ }
+
+ public Label getChannelConfigLabel()
+ {
+ if(mChannelConfigLabel == null)
+ {
+ mChannelConfigLabel = new Label();
+ }
+
+ return mChannelConfigLabel;
+ }
+
+ public Label getChannelDescriptorLabel()
+ {
+ if(mChannelDescriptorLabel == null)
+ {
+ mChannelDescriptorLabel = new Label();
+ }
+
+ return mChannelDescriptorLabel;
+ }
+
+ private Label getTrafficChannelManagerLabel()
+ {
+ if(mTrafficChannelManagerLabel == null)
+ {
+ mTrafficChannelManagerLabel = new Label();
+
+ }
+
+ return mTrafficChannelManagerLabel;
+ }
+
+ private Label getPreloadDataContentLabel()
+ {
+ if(mPreloadDataContentLabel == null)
+ {
+ mPreloadDataContentLabel = new Label();
+ }
+
+ return mPreloadDataContentLabel;
+ }
+
+ private Label getParentDecodeHistoryLabel()
+ {
+ if(mParentDecodeHistoryLabel == null)
+ {
+ mParentDecodeHistoryLabel = new Label();
+ }
+
+ return mParentDecodeHistoryLabel;
+ }
+
+ private Label getChildDecodeHistoryLabel()
+ {
+ if(mChildDecodeHistoryLabel == null)
+ {
+ mChildDecodeHistoryLabel = new Label();
+ }
+
+ return mChildDecodeHistoryLabel;
+ }
+
+ private IdentifierCollectionViewer getIdentifierCollectionViewer()
+ {
+ if(mIdentifierCollectionViewer == null)
+ {
+ mIdentifierCollectionViewer = new IdentifierCollectionViewer();
+ }
+
+ return mIdentifierCollectionViewer;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/IdentifierCollectionViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/IdentifierCollectionViewer.java
new file mode 100644
index 000000000..27e5af6b7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/IdentifierCollectionViewer.java
@@ -0,0 +1,90 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+/**
+ * JavaFX identifier collection viewer
+ */
+public class IdentifierCollectionViewer extends VBox
+{
+ private TableView mIdentifierTableView;
+
+ /**
+ * Constructs an instance
+ */
+ public IdentifierCollectionViewer()
+ {
+ GridPane gridPane = new GridPane();
+ GridPane.setHgrow(getIdentifierTableView(), Priority.ALWAYS);
+ gridPane.add(getIdentifierTableView(), 0, 0);
+ getChildren().add(gridPane);
+ }
+
+ public void set(IdentifierCollection identifierCollection)
+ {
+ getIdentifierTableView().getItems().clear();
+
+ if(identifierCollection != null)
+ {
+ getIdentifierTableView().getItems().addAll(identifierCollection.getIdentifiers());
+ }
+ }
+
+ public TableView getIdentifierTableView()
+ {
+ if(mIdentifierTableView == null)
+ {
+ mIdentifierTableView = new TableView<>();
+
+ TableColumn classColumn = new TableColumn();
+ classColumn.setPrefWidth(110);
+ classColumn.setText("Class");
+ classColumn.setCellValueFactory(new PropertyValueFactory<>("identifierClass"));
+
+ TableColumn formColumn = new TableColumn();
+ formColumn.setPrefWidth(160);
+ formColumn.setText("Form");
+ formColumn.setCellValueFactory(new PropertyValueFactory<>("form"));
+
+ TableColumn roleColumn = new TableColumn();
+ roleColumn.setPrefWidth(110);
+ roleColumn.setText("Role");
+ roleColumn.setCellValueFactory(new PropertyValueFactory<>("role"));
+
+ TableColumn valueColumn = new TableColumn();
+ valueColumn.setPrefWidth(160);
+ valueColumn.setText("Value");
+ valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
+
+ mIdentifierTableView.getColumns().addAll(classColumn, formColumn, roleColumn, valueColumn);
+ }
+
+ return mIdentifierTableView;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
new file mode 100644
index 000000000..a912f88c8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
@@ -0,0 +1,159 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
+import io.github.dsheirer.module.decode.event.IDecodeEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper class that combines a single message with any decoder state events and decoded events that were produced by a
+ * decoder state.
+ */
+public class MessagePackage
+{
+ private IMessage mMessage;
+ private List mDecoderStateEvents = new ArrayList<>();
+ private List mDecodeEvents = new ArrayList<>();
+ private ChannelStartProcessingRequest mChannelStartProcessingRequest;
+
+ /**
+ * Constructs an instance
+ * @param message for this instance
+ */
+ public MessagePackage(IMessage message)
+ {
+ mMessage = message;
+ }
+
+ /**
+ * Message for this instance
+ */
+ public IMessage getMessage()
+ {
+ return mMessage;
+ }
+
+ /**
+ * List of decoder state events
+ */
+ public List getDecoderStateEvents()
+ {
+ return mDecoderStateEvents;
+ }
+
+ /**
+ * Adds the decoder state event to this instance
+ */
+ public void add(DecoderStateEvent event)
+ {
+ mDecoderStateEvents.add(event);
+ }
+
+ /**
+ * List of decode event snapshots
+ */
+ public List getDecodeEvents()
+ {
+ return mDecodeEvents;
+ }
+
+ /**
+ * Adds the decode event to this instance
+ */
+ public void add(DecodeEventSnapshot event)
+ {
+ mDecodeEvents.add(event);
+ }
+
+ /**
+ * Adds the channel start processing request
+ */
+ public void add(ChannelStartProcessingRequest request)
+ {
+ mChannelStartProcessingRequest = request;
+ }
+
+ /**
+ * Message timeslot
+ */
+ public int getTimeslot()
+ {
+ return getMessage().getTimeslot();
+ }
+
+ /**
+ * Message timestamp
+ */
+ public long getTimestamp()
+ {
+ return getMessage().getTimestamp();
+ }
+
+ /**
+ * Message validity flag
+ */
+ public boolean isValid()
+ {
+ return getMessage().isValid();
+ }
+
+ /**
+ * Message string representation
+ */
+ @Override
+ public String toString()
+ {
+ return getMessage().toString();
+ }
+
+ /**
+ * Count of decode events
+ */
+ public int getDecodeEventCount()
+ {
+ return mDecodeEvents.size();
+ }
+
+ /**
+ * Count of decoder state events
+ */
+ public int getDecoderStateEventCount()
+ {
+ return mDecoderStateEvents.size();
+ }
+
+ /**
+ * Count (0 or 1) of channel start processing requests.
+ */
+ public int getChannelStartProcessingRequestCount()
+ {
+ return mChannelStartProcessingRequest == null ? 0 : 1;
+ }
+
+ public ChannelStartProcessingRequest getChannelStartProcessingRequest()
+ {
+ return mChannelStartProcessingRequest;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java
new file mode 100644
index 000000000..061c71c05
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackageViewer.java
@@ -0,0 +1,225 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+/**
+ * JavaFX message package details viewer
+ */
+public class MessagePackageViewer extends VBox
+{
+ private Label mMessageLabel;
+ private TableView mDecoderStateEventTableView;
+ private TableView mDecodeEventTableView;
+ private IdentifierCollectionViewer mIdentifierCollectionViewer;
+ private ChannelStartProcessingRequestViewer mChannelStartProcessingRequestViewer;
+
+ /**
+ * Constructs an instance
+ */
+ public MessagePackageViewer()
+ {
+ GridPane gridPane = new GridPane();
+ gridPane.setHgap(5);
+ gridPane.setVgap(5);
+ Label messageLabel = new Label("Message:");
+ gridPane.add(messageLabel, 0, 0);
+ GridPane.setHgrow(getMessageLabel(), Priority.ALWAYS);
+ gridPane.add(getMessageLabel(), 1, 0);
+
+ gridPane.add(new Label("Decoder State Events"), 0, 1);
+ getDecoderStateEventTableView().setPrefHeight(120);
+ GridPane.setHgrow(getDecoderStateEventTableView(), Priority.NEVER);
+ gridPane.add(getDecoderStateEventTableView(), 0, 2);
+
+ gridPane.add(new Label("Decode Events"), 1, 1);
+ getDecodeEventTableView().setPrefHeight(120);
+ GridPane.setHgrow(getDecodeEventTableView(), Priority.ALWAYS);
+ gridPane.add(getDecodeEventTableView(), 1, 2);
+
+ gridPane.add(new Label("Channel Start Processing Request"), 0, 3);
+ gridPane.add(getChannelStartProcessingRequestViewer(), 0, 4);
+
+ getIdentifierCollectionViewer().setPrefHeight(120);
+ gridPane.add(new Label("Selected Decode Event Identifiers"), 1, 3);
+ gridPane.add(getIdentifierCollectionViewer(), 1, 4);
+
+ getChildren().add(gridPane);
+ }
+
+ public void set(MessagePackage messagePackage)
+ {
+ getMessageLabel().setText(null);
+ getDecoderStateEventTableView().getItems().clear();
+ getDecodeEventTableView().getItems().clear();
+ getChannelStartProcessingRequestViewer().set(null);
+
+ if(messagePackage != null)
+ {
+ String message = messagePackage.toString();
+ if(message.length() > 40)
+ {
+ message = message.substring(0, 40) + "...";
+ }
+ getMessageLabel().setText(message);
+ getDecoderStateEventTableView().getItems().addAll(messagePackage.getDecoderStateEvents());
+ getDecodeEventTableView().getItems().addAll(messagePackage.getDecodeEvents());
+ getChannelStartProcessingRequestViewer().set(messagePackage.getChannelStartProcessingRequest());
+
+ if(getDecodeEventTableView().getItems().size() > 0)
+ {
+ getDecodeEventTableView().getSelectionModel().select(0);
+ }
+ }
+ }
+
+ private IdentifierCollectionViewer getIdentifierCollectionViewer()
+ {
+ if(mIdentifierCollectionViewer == null)
+ {
+ mIdentifierCollectionViewer = new IdentifierCollectionViewer();
+ }
+
+ return mIdentifierCollectionViewer;
+ }
+
+ private Label getMessageLabel()
+ {
+ if(mMessageLabel == null)
+ {
+ mMessageLabel = new Label();
+ mMessageLabel.setMaxWidth(Double.MAX_VALUE);
+ }
+
+ return mMessageLabel;
+ }
+
+ public TableView getDecoderStateEventTableView()
+ {
+ if(mDecoderStateEventTableView == null)
+ {
+ mDecoderStateEventTableView = new TableView<>();
+
+ TableColumn timeslotColumn = new TableColumn();
+ timeslotColumn.setPrefWidth(110);
+ timeslotColumn.setText("Timeslot");
+ timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
+
+ TableColumn stateColumn = new TableColumn();
+ stateColumn.setPrefWidth(110);
+ stateColumn.setText("State");
+ stateColumn.setCellValueFactory(new PropertyValueFactory<>("state"));
+
+ TableColumn eventColumn = new TableColumn();
+ eventColumn.setPrefWidth(110);
+ eventColumn.setText("Event");
+ eventColumn.setCellValueFactory(new PropertyValueFactory<>("event"));
+
+ TableColumn frequencyColumn = new TableColumn();
+ frequencyColumn.setPrefWidth(110);
+ frequencyColumn.setText("Frequency");
+ frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
+
+ mDecoderStateEventTableView.getColumns().addAll(timeslotColumn, stateColumn, eventColumn, frequencyColumn);
+ }
+
+ return mDecoderStateEventTableView;
+ }
+
+ private TableView getDecodeEventTableView()
+ {
+ if(mDecodeEventTableView == null)
+ {
+ mDecodeEventTableView = new TableView<>();
+ mDecodeEventTableView.setMaxWidth(Double.MAX_VALUE);
+
+ TableColumn startTimeColumn = new TableColumn();
+ startTimeColumn.setPrefWidth(110);
+ startTimeColumn.setText("Start");
+ startTimeColumn.setCellValueFactory(new PropertyValueFactory<>("timeStart"));
+
+ TableColumn durationColumn = new TableColumn();
+ durationColumn.setPrefWidth(110);
+ durationColumn.setText("Duration");
+ durationColumn.setCellValueFactory(new PropertyValueFactory<>("duration"));
+
+ TableColumn typeColumn = new TableColumn();
+ typeColumn.setPrefWidth(130);
+ typeColumn.setText("Type");
+ typeColumn.setCellValueFactory(new PropertyValueFactory<>("eventType"));
+
+ TableColumn channelDescriptorColumn = new TableColumn();
+ channelDescriptorColumn.setPrefWidth(110);
+ channelDescriptorColumn.setText("Channel");
+ channelDescriptorColumn.setCellValueFactory(new PropertyValueFactory<>("channelDescriptor"));
+
+ TableColumn frequencyColumn = new TableColumn();
+ frequencyColumn.setPrefWidth(110);
+ frequencyColumn.setText("Frequency");
+ frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
+
+ TableColumn hashcodeColumn = new TableColumn();
+ hashcodeColumn.setPrefWidth(100);
+ hashcodeColumn.setText("Hash ID");
+ hashcodeColumn.setCellValueFactory(new PropertyValueFactory<>("originalHashCode"));
+
+ TableColumn detailsColumn = new TableColumn();
+ detailsColumn.setPrefWidth(500);
+ detailsColumn.setText("Details");
+ detailsColumn.setCellValueFactory(new PropertyValueFactory<>("details"));
+
+ mDecodeEventTableView.getColumns().addAll(startTimeColumn, durationColumn, typeColumn, channelDescriptorColumn,
+ frequencyColumn, hashcodeColumn, detailsColumn);
+
+ mDecodeEventTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
+ {
+ if(newValue != null)
+ {
+ getIdentifierCollectionViewer().set(newValue.getIdentifierCollection());
+ }
+ else
+ {
+ getIdentifierCollectionViewer().set(null);
+ }
+ });
+ }
+
+ return mDecodeEventTableView;
+ }
+
+ private ChannelStartProcessingRequestViewer getChannelStartProcessingRequestViewer()
+ {
+ if(mChannelStartProcessingRequestViewer == null)
+ {
+ mChannelStartProcessingRequestViewer = new ChannelStartProcessingRequestViewer();
+ }
+
+ return mChannelStartProcessingRequestViewer;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
new file mode 100644
index 000000000..ef5fb0744
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
@@ -0,0 +1,107 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import com.google.common.eventbus.Subscribe;
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
+import io.github.dsheirer.module.decode.event.IDecodeEvent;
+import io.github.dsheirer.module.decode.event.IDecodeEventListener;
+
+/**
+ * Utility for combining a message and decoder state events.
+ */
+public class MessagePackager
+{
+ private MessagePackage mMessagePackage;
+
+ /**
+ * Constructs an instance
+ */
+ public MessagePackager()
+ {
+ }
+
+ /**
+ * Adds the message and creates a new MessageWithEvents instance, wrapping the message, ready to also receive any
+ * decode events and decoder state events. The previous message with events is overwritten.
+ * @param message to wrap.
+ */
+ public void add(IMessage message)
+ {
+ mMessagePackage = new MessagePackage(message);
+ }
+
+ /**
+ * Access the current message with events.
+ */
+ public MessagePackage getMessageWithEvents()
+ {
+ return mMessagePackage;
+ }
+
+ /**
+ * Adds the decoder state event to the current message with events.
+ * @param event to add
+ */
+ public void add(DecoderStateEvent event)
+ {
+ if(mMessagePackage != null)
+ {
+ mMessagePackage.add(event);
+ }
+ }
+
+ /**
+ * Adds the decode event to the current message with events.
+ * @param event to add
+ */
+ public void add(IDecodeEvent event)
+ {
+ if(mMessagePackage != null)
+ {
+ if(event instanceof DecodeEvent decodeEvent)
+ {
+ try
+ {
+ DecodeEventSnapshot snapshot = decodeEvent.getSnapshot();
+ mMessagePackage.add(snapshot);
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Subscription to receive channel start processing requests via the event bus from the traffic channel manager
+ * @param request sent from the traffic channel manager.
+ */
+ @Subscribe
+ public void process(ChannelStartProcessingRequest request)
+ {
+ mMessagePackage.add(request);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
similarity index 86%
rename from src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
rename to src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
index c3a4952c7..6729fb1a4 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -41,20 +41,21 @@
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
- * Supported Protocols: DMR and APCO25 Phase 1.
+ * Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
*/
-public class RecordingViewer extends VBox
+public class MessageRecordingViewer extends VBox
{
- private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
+ private static final Logger mLog = LoggerFactory.getLogger(MessageRecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
+ private int mTabCounterP25P2 = 1;
/**
* Constructs an instance
*/
- public RecordingViewer()
+ public MessageRecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
@@ -74,13 +75,19 @@ public MenuBar getMenuBar()
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
- MenuItem p25p1MenuItem = new MenuItem("P25P1");
+ MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
p25p1MenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
- createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
+ MenuItem p25p2MenuItem = new MenuItem("P25 Phase 2");
+ p25p2MenuItem.onActionProperty().set(event -> {
+ Tab tab = new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer());
+ getTabPane().getTabs().add(tab);
+ getTabPane().getSelectionModel().select(tab);
+ });
+ createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
@@ -102,6 +109,7 @@ public TabPane getTabPane()
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
+ mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
}
return mTabPane;
@@ -151,11 +159,6 @@ public LabeledTab(String label, Node node)
}
});
}
-
- public LabeledTab(String label)
- {
- this(label, null);
- }
}
public static void main(String[] args)
@@ -165,7 +168,7 @@ public static void main(String[] args)
@Override
public void start(Stage primaryStage) throws Exception
{
- Scene scene = new Scene(new RecordingViewer(), 1100, 800);
+ Scene scene = new Scene(new MessageRecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
index 997761e70..d50c9d555 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,8 +19,14 @@
package io.github.dsheirer.gui.viewer;
-import io.github.dsheirer.message.IMessage;
+import com.google.common.eventbus.EventBus;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.identifier.IdentifierUpdateNotification;
+import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.message.StuffBitsMessage;
+import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
+import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
+import io.github.dsheirer.module.decode.p25.phase1.P25P1DecoderState;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
import io.github.dsheirer.record.binary.BinaryReader;
@@ -33,8 +39,11 @@
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -71,17 +80,20 @@ public class P25P1Viewer extends VBox
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.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.p25p1";
+ private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
- private TableView mMessageTableView;
- private ObservableList mMessages = FXCollections.observableArrayList();
- private FilteredList mFilteredMessages = new FilteredList<>(mMessages);
+ private TableView mMessagePackageTableView;
+ private ObservableList mMessagePackages = FXCollections.observableArrayList();
+ private FilteredList mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
+ private MessagePackageViewer mMessagePackageViewer;
+ private StringProperty mLoadedFile = new SimpleStringProperty();
public P25P1Viewer()
{
@@ -115,9 +127,11 @@ public P25P1Viewer()
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
- VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
+ VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
+ VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
+
+ getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
- getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
@@ -132,19 +146,54 @@ private void load(File file)
{
if(file != null && file.exists())
{
- mMessages.clear();
+ mLoadedFile.set(file.toString());
+ mMessagePackages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
- List messages = new ArrayList<>();
+ List messagePackages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
+
+ Channel empty = new Channel("Empty");
+ empty.setDecodeConfiguration(new DecodeConfigP25Phase1());
+
+ MessagePackager messagePackager = new MessagePackager();
+
+ //Setup a temporary event bus to capture channel start processing requests
+ EventBus eventBus = new EventBus("debug");
+ eventBus.register(messagePackager);
+ P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
+ trafficChannelManager.setInterModuleEventBus(eventBus);
+
+ //Register to receive events
+ trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
+ P25P1DecoderState decoderState = new P25P1DecoderState(empty, trafficChannelManager);
+ decoderState.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
+ decoderState.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
+ decoderState.start();
+
+ long frequency = getFrequencyFromFile(mLoadedFile.get());
+
+ if(frequency > 0)
+ {
+ trafficChannelManager.setCurrentControlFrequency(frequency, empty);
+ FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
+ decoderState.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
+ IdentifierUpdateNotification.Operation.ADD, 1));
+ }
+
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
- messages.add(message);
+ //Add the initial message to the packager so that it can be combined with any decoder state events.
+ messagePackager.add(message);
+ decoderState.receive(message);
+
+ //Collect the packaged message with events
+ messagePackages.add(messagePackager.getMessageWithEvents());
}
});
@@ -164,13 +213,47 @@ private void load(File file)
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
- mMessages.addAll(messages);
- getMessageTableView().scrollTo(0);
+ mMessagePackages.addAll(messagePackages);
+ getMessagePackageTableView().scrollTo(0);
});
});
}
}
+ /**
+ * Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
+ * the decoder states.
+ * @param file name to parse
+ * @return parsed frequency or zero.
+ */
+ private static long getFrequencyFromFile(String file)
+ {
+ if(file == null || file.isEmpty())
+ {
+ return 0;
+ }
+
+ if(file.matches(FILE_FREQUENCY_REGEX))
+ {
+ Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
+ Matcher m = p.matcher(file);
+ if(m.find())
+ {
+ try
+ {
+ String raw = m.group(1);
+ return Long.parseLong(raw);
+ }
+ catch(Exception e)
+ {
+ mLog.error("Couldn't parse frequency from bits file [" + file + "]");
+ }
+ }
+ }
+
+ return 0;
+ }
+
/**
* Updates the filter(s) applies to the list of messages
*/
@@ -180,12 +263,12 @@ private void updateFilters()
if(filterText != null && !filterText.isEmpty())
{
- Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
- mFilteredMessages.setPredicate(textPredicate);
+ Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
+ mFilteredMessagePackages.setPredicate(textPredicate);
}
else
{
- mFilteredMessages.setPredicate(null);
+ mFilteredMessagePackages.setPredicate(null);
}
}
@@ -197,12 +280,12 @@ private void find(String text)
{
if(text != null && !text.isEmpty())
{
- for(IMessage message: mFilteredMessages)
+ for(MessagePackage messagePackage: mFilteredMessagePackages)
{
- if(message.toString().toLowerCase().contains(text.toLowerCase()))
+ if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
- getMessageTableView().getSelectionModel().select(message);
- getMessageTableView().scrollTo(message);
+ getMessagePackageTableView().getSelectionModel().select(messagePackage);
+ getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
@@ -217,7 +300,7 @@ private void findNext(String text)
{
if(text != null && !text.isEmpty())
{
- IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
+ MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
@@ -225,18 +308,18 @@ private void findNext(String text)
return;
}
- int row = mFilteredMessages.indexOf(selected);
+ int row = mFilteredMessagePackages.indexOf(selected);
- for(int x = row + 1; x < mFilteredMessages.size(); x++)
+ for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
{
- if(x < mFilteredMessages.size())
+ if(x < mFilteredMessagePackages.size())
{
- IMessage message = mFilteredMessages.get(x);
+ MessagePackage messagePackage = mFilteredMessagePackages.get(x);
- if(message.toString().toLowerCase().contains(text.toLowerCase()))
+ if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
- getMessageTableView().getSelectionModel().select(message);
- getMessageTableView().scrollTo(message);
+ getMessagePackageTableView().getSelectionModel().select(messagePackage);
+ getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
@@ -244,25 +327,40 @@ private void findNext(String text)
}
}
+ private MessagePackageViewer getMessagePackageViewer()
+ {
+ if(mMessagePackageViewer == null)
+ {
+ mMessagePackageViewer = new MessagePackageViewer();
+ mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
+
+ //Register for table selection events to display the selected value.
+ getMessagePackageTableView().getSelectionModel().selectedItemProperty()
+ .addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
+ }
+
+ return mMessagePackageViewer;
+ }
+
/**
- * List view control with DMR messages
+ * List view control with messages
*/
- private TableView getMessageTableView()
+ private TableView getMessagePackageTableView()
{
- if(mMessageTableView == null)
+ if(mMessagePackageTableView == null)
{
- mMessageTableView = new TableView<>();
- mMessageTableView.setPlaceholder(getLoadingIndicator());
- SortedList sortedList = new SortedList<>(mFilteredMessages);
- sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
- mMessageTableView.setItems(sortedList);
+ mMessagePackageTableView = new TableView<>();
+ mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
+ SortedList sortedList = new SortedList<>(mFilteredMessagePackages);
+ sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
+ mMessagePackageTableView.setItems(sortedList);
- mMessageTableView.setOnKeyPressed(event ->
+ mMessagePackageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set rows = new TreeSet<>();
- for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
+ for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
@@ -282,7 +380,7 @@ private TableView getMessageTableView()
boolean firstCol = true;
- for (final TableColumn, ?> column : mMessageTableView.getColumns())
+ for (final TableColumn, ?> column : mMessagePackageTableView.getColumns())
{
if(firstCol)
{
@@ -313,23 +411,44 @@ private TableView getMessageTableView()
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
+ TableColumn timeslotColumn = new TableColumn();
+ timeslotColumn.setPrefWidth(35);
+ timeslotColumn.setText("TS");
+ timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
+
TableColumn messageColumn = new TableColumn();
- messageColumn.setPrefWidth(1000);
+ messageColumn.setPrefWidth(900);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback) param -> {
SimpleStringProperty property = new SimpleStringProperty();
- if(param.getValue() instanceof IMessage message)
+ if(param.getValue() instanceof MessagePackage messagePackage)
{
- property.set(message.toString());
+ property.set(messagePackage.toString());
}
return property;
});
- mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
+ TableColumn decodeEventCountColumn = new TableColumn();
+ decodeEventCountColumn.setPrefWidth(50);
+ decodeEventCountColumn.setText("Events");
+ decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
+
+ TableColumn decoderStateEventCountColumn = new TableColumn();
+ decoderStateEventCountColumn.setPrefWidth(50);
+ decoderStateEventCountColumn.setText("States");
+ decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
+
+ TableColumn channelStartCountColumn = new TableColumn();
+ channelStartCountColumn.setPrefWidth(50);
+ channelStartCountColumn.setText("Starts");
+ channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
+
+ mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
+ decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
}
- return mMessageTableView;
+ return mMessagePackageTableView;
}
/**
@@ -343,7 +462,7 @@ private Button getSelectFileButton()
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle("Select DMR .bits Recording");
+ fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
new file mode 100644
index 000000000..94723b6b8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
@@ -0,0 +1,738 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import com.google.common.eventbus.EventBus;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.gui.control.IntegerTextField;
+import io.github.dsheirer.identifier.IdentifierUpdateNotification;
+import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
+import io.github.dsheirer.identifier.patch.PatchGroupManager;
+import io.github.dsheirer.message.StuffBitsMessage;
+import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
+import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
+import io.github.dsheirer.module.decode.p25.phase1.message.P25FrequencyBand;
+import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
+import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
+import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
+import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageFramer;
+import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageProcessor;
+import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
+import io.github.dsheirer.record.binary.BinaryReader;
+import io.github.dsheirer.util.ThreadPool;
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.prefs.Preferences;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextField;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import javafx.util.Callback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * APCO25 Phase 2 viewer panel
+ */
+public class P25P2Viewer extends VBox
+{
+ private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.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.p25p2";
+ private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
+ private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.class);
+ private Button mSelectFileButton;
+ private Label mSelectedFileLabel;
+ private TableView mMessagePackageTableView;
+ private ObservableList mMessagePackages = FXCollections.observableArrayList();
+ private FilteredList mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
+ private CheckBox mShowTS0;
+ private CheckBox mShowTS1;
+ private CheckBox mShowTS2;
+ private TextField mSearchText;
+ private TextField mFindText;
+ private Button mFindButton;
+ private Button mFindNextButton;
+ private ProgressIndicator mLoadingIndicator;
+ private IntegerTextField mWACNTextField;
+ private IntegerTextField mSystemTextField;
+ private IntegerTextField mNACTextField;
+ private Button mReloadButton;
+ private StringProperty mLoadedFile = new SimpleStringProperty();
+ private MessagePackageViewer mMessagePackageViewer;
+
+ public P25P2Viewer()
+ {
+ setPadding(new Insets(5));
+ setSpacing(5);
+
+ HBox fileBox = new HBox();
+ fileBox.setMaxWidth(Double.MAX_VALUE);
+ fileBox.setAlignment(Pos.CENTER_LEFT);
+ fileBox.setSpacing(5);
+ getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
+
+ HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
+ HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
+ fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
+
+ HBox scrambleSettingsBox = new HBox();
+ scrambleSettingsBox.setAlignment(Pos.BASELINE_LEFT);
+ scrambleSettingsBox.setSpacing(5);
+ Label wacnLabel = new Label("WACN:");
+ Label systemLabel = new Label("SYSTEM:");
+ Label nacLabel = new Label("NAC:");
+ scrambleSettingsBox.getChildren().addAll(wacnLabel, getWACNTextField(), systemLabel, getSystemTextField(),
+ nacLabel, getNACTextField(), getReloadButton());
+
+ HBox filterBox = new HBox();
+ filterBox.setMaxWidth(Double.MAX_VALUE);
+ filterBox.setAlignment(Pos.BASELINE_CENTER);
+ filterBox.setSpacing(5);
+
+ Label searchLabel = new Label("Message Filter:");
+ HBox.setMargin(searchLabel, new Insets(0,0,0,15));
+ Label findLabel = new Label("Find:");
+
+ HBox.setHgrow(getFindText(), Priority.ALWAYS);
+ HBox.setHgrow(getSearchText(), Priority.ALWAYS);
+
+ Label showLabel = new Label("Show:");
+ HBox.setMargin(showLabel, new Insets(0,0,0,15));
+
+ filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
+ getSearchText(), showLabel, getShowTS0(), getShowTS1(), getShowTS2());
+
+ VBox.setVgrow(fileBox, Priority.NEVER);
+ VBox.setVgrow(filterBox, Priority.NEVER);
+ VBox.setVgrow(scrambleSettingsBox, Priority.NEVER);
+ VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
+ VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
+
+ getChildren().addAll(fileBox, scrambleSettingsBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
+ }
+
+ /**
+ * Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
+ * the decoder states.
+ * @param file name to parse
+ * @return parsed frequency or zero.
+ */
+ private static long getFrequencyFromFile(String file)
+ {
+ if(file == null || file.isEmpty())
+ {
+ return 0;
+ }
+
+ if(file.matches(FILE_FREQUENCY_REGEX))
+ {
+ Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
+ Matcher m = p.matcher(file);
+ if(m.find())
+ {
+ try
+ {
+ String raw = m.group(1);
+ return Long.parseLong(raw);
+ }
+ catch(Exception e)
+ {
+ mLog.error("Couldn't parse frequency from bits file [" + file + "]");
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Processes the recording file and loads the content into the viewer
+ *
+ * Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
+ * message table back on the JavaFX UI thread.
+ *
+ * @param file containing a .bits recording of decoded data.
+ */
+ private void load(File file)
+ {
+ mLog.info("Loading File: " + file);
+ if(file != null && file.exists())
+ {
+ mLoadedFile.set(file.toString());
+ mMessagePackages.clear();
+ getLoadingIndicator().setVisible(true);
+ getSelectedFileLabel().setText("Loading ...");
+
+ int wacn = getWACNTextField().get();
+ int system = getSystemTextField().get();
+ int nac = getNACTextField().get();
+ ScrambleParameters scrambleParameters = new ScrambleParameters(wacn, system, nac);
+
+ ThreadPool.CACHED.submit(() -> {
+ List messages = new ArrayList<>();
+ P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, 9600);
+ messageFramer.setScrambleParameters(scrambleParameters);
+ P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
+ messageFramer.setListener(messageProcessor);
+ Channel empty = new Channel("Empty");
+ empty.setDecodeConfiguration(new DecodeConfigP25Phase2());
+
+ MessagePackager messagePackager = new MessagePackager();
+
+ //Setup a temporary event bus to capture channel start processing requests
+ EventBus eventBus = new EventBus("debug");
+ eventBus.register(messagePackager);
+ P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
+ trafficChannelManager.setInterModuleEventBus(eventBus);
+
+ //Register to receive events
+ trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
+ PatchGroupManager patchGroupManager = new PatchGroupManager();
+ P25P2DecoderState decoderState1 = new P25P2DecoderState(empty, 1, trafficChannelManager,
+ patchGroupManager);
+ decoderState1.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
+ decoderState1.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
+ P25P2DecoderState decoderState2 = new P25P2DecoderState(empty, 2, trafficChannelManager,
+ patchGroupManager);
+ decoderState2.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
+ decoderState2.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
+ decoderState1.start();
+ decoderState2.start();
+
+ long frequency = getFrequencyFromFile(mLoadedFile.get());
+
+ if(frequency > 0)
+ {
+ trafficChannelManager.setCurrentControlFrequency(frequency, empty);
+ FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
+ decoderState1.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
+ IdentifierUpdateNotification.Operation.ADD, 1));
+ decoderState2.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
+ IdentifierUpdateNotification.Operation.ADD, 2));
+ }
+
+ //TODO: testing use - PCWIN TDMA Frequency Band as preload data
+ //ID:2 OFFSET:-45000000 SPACING:12500 BASE:851012500 TDMA BW:12500 TIMESLOTS:2 VOCODER:HALF_RATE
+ P25FrequencyBand band = new P25FrequencyBand(2, 851012500l, -45000000l, 12500, 12500, 2);
+ P25FrequencyBandPreloadDataContent content = new P25FrequencyBandPreloadDataContent(Collections.singleton(band));
+ messageProcessor.preload(content);
+
+ messageProcessor.setMessageListener(message -> {
+ if(!(message instanceof StuffBitsMessage))
+ {
+// System.out.println(message);
+ //Add the initial message to the packager so that it can be combined with any decoder state events.
+ messagePackager.add(message);
+ if(message.getTimeslot() == P25P1Message.TIMESLOT_1)
+ {
+ decoderState1.receive(message);
+ }
+ else if(message.getTimeslot() == P25P1Message.TIMESLOT_2)
+ {
+ decoderState2.receive(message);
+ }
+
+ //Collect the packaged message with events
+ messages.add(messagePackager.getMessageWithEvents());
+ }
+ });
+
+ try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
+ {
+ while(reader.hasNext())
+ {
+ ByteBuffer buffer = reader.next();
+// System.out.println("Processing Bytes " + buffer.capacity() + " / " + reader.getByteCounter());
+ messageFramer.receive(buffer);
+ }
+ }
+ catch(Exception ioe)
+ {
+ ioe.printStackTrace();
+ }
+
+ Platform.runLater(() -> {
+ getLoadingIndicator().setVisible(false);
+ getSelectedFileLabel().setText(file.getName());
+ mMessagePackages.addAll(messages);
+ getMessagePackageTableView().scrollTo(0);
+ });
+ });
+ }
+ else
+ {
+ mLog.info("Can't load file: " + file);
+ }
+ }
+
+ /**
+ * Updates the filter(s) applies to the list of messages
+ */
+ private void updateFilters()
+ {
+ Predicate timeslotPredicate = message ->
+ (getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
+ (getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
+ (getShowTS2().isSelected() && (message.getTimeslot() == 2));
+
+ String filterText = getSearchText().getText();
+
+ if(filterText == null || filterText.isEmpty())
+ {
+ mFilteredMessagePackages.setPredicate(timeslotPredicate);
+ }
+ else
+ {
+ Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
+ mFilteredMessagePackages.setPredicate(timeslotPredicate.and(textPredicate));
+ }
+ }
+
+ /**
+ * Finds and selects the first row containing the text argument.
+ * @param text to search for.
+ */
+ private void find(String text)
+ {
+ if(text != null && !text.isEmpty())
+ {
+ for(MessagePackage messagePackage: mFilteredMessagePackages)
+ {
+ if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
+ {
+ getMessagePackageTableView().getSelectionModel().select(messagePackage);
+ getMessagePackageTableView().scrollTo(messagePackage);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Finds and selects the first row containing the text argument, after the currently selected row.
+ * @param text to search for.
+ */
+ private void findNext(String text)
+ {
+ if(text != null && !text.isEmpty())
+ {
+ MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
+
+ if(selected == null)
+ {
+ find(text);
+ return;
+ }
+
+ int row = mFilteredMessagePackages.indexOf(selected);
+
+ for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
+ {
+ if(x < mFilteredMessagePackages.size())
+ {
+ MessagePackage messagePackage = mFilteredMessagePackages.get(x);
+
+ if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
+ {
+ getMessagePackageTableView().getSelectionModel().select(messagePackage);
+ getMessagePackageTableView().scrollTo(messagePackage);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ private MessagePackageViewer getMessagePackageViewer()
+ {
+ if(mMessagePackageViewer == null)
+ {
+ mMessagePackageViewer = new MessagePackageViewer();
+ mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
+
+ //Register for table selection events to display the selected value.
+ getMessagePackageTableView().getSelectionModel().selectedItemProperty()
+ .addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
+ }
+
+ return mMessagePackageViewer;
+ }
+
+ private IntegerTextField getWACNTextField()
+ {
+ if(mWACNTextField == null)
+ {
+ mWACNTextField = new IntegerTextField();
+ }
+
+ return mWACNTextField;
+ }
+
+ private IntegerTextField getSystemTextField()
+ {
+ if(mSystemTextField == null)
+ {
+ mSystemTextField = new IntegerTextField();
+ }
+
+ return mSystemTextField;
+ }
+
+ private IntegerTextField getNACTextField()
+ {
+ if(mNACTextField == null)
+ {
+ mNACTextField = new IntegerTextField();
+ }
+
+ return mNACTextField;
+ }
+
+ private Button getReloadButton()
+ {
+ if(mReloadButton == null)
+ {
+ mReloadButton = new Button("Reload");
+ mReloadButton.disableProperty().bind(Bindings.isNull(mLoadedFile));
+ mReloadButton.setOnAction(event -> load(new File(mLoadedFile.get())));
+ }
+
+ return mReloadButton;
+ }
+
+ /**
+ * List view control with message packages
+ */
+ private TableView getMessagePackageTableView()
+ {
+ if(mMessagePackageTableView == null)
+ {
+ mMessagePackageTableView = new TableView<>();
+ mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
+ SortedList sortedList = new SortedList<>(mFilteredMessagePackages);
+ sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
+ mMessagePackageTableView.setItems(sortedList);
+
+ mMessagePackageTableView.setOnKeyPressed(event ->
+ {
+ if(KEY_CODE_COPY.match(event))
+ {
+ final Set rows = new TreeSet<>();
+ for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
+ {
+ rows.add(tablePosition.getRow());
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ boolean firstRow = true;
+ for (final Integer row : rows)
+ {
+ if(firstRow)
+ {
+ firstRow = false;
+ }
+ else
+ {
+ sb.append('\n');
+ }
+
+ boolean firstCol = true;
+
+ for (final TableColumn, ?> column : mMessagePackageTableView.getColumns())
+ {
+ if(firstCol)
+ {
+ firstCol = false;
+ }
+ else
+ {
+ sb.append('\t');
+ }
+
+ final Object cellData = column.getCellData(row);
+ sb.append(cellData == null ? "" : cellData.toString());
+ }
+ }
+ final ClipboardContent clipboardContent = new ClipboardContent();
+ clipboardContent.putString(sb.toString());
+ Clipboard.getSystemClipboard().setContent(clipboardContent);
+ }
+ });
+
+ TableColumn timestampColumn = new TableColumn();
+ timestampColumn.setPrefWidth(110);
+ timestampColumn.setText("Time");
+ timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
+
+ TableColumn validColumn = new TableColumn();
+ validColumn.setPrefWidth(50);
+ validColumn.setText("Valid");
+ validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
+
+ TableColumn timeslotColumn = new TableColumn();
+ timeslotColumn.setPrefWidth(35);
+ timeslotColumn.setText("TS");
+ timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
+
+ TableColumn messageColumn = new TableColumn();
+ messageColumn.setPrefWidth(900);
+ messageColumn.setText("Message");
+ messageColumn.setCellValueFactory((Callback) param -> {
+ SimpleStringProperty property = new SimpleStringProperty();
+ if(param.getValue() instanceof MessagePackage messagePackage)
+ {
+ property.set(messagePackage.toString());
+ }
+
+ return property;
+ });
+
+ TableColumn decodeEventCountColumn = new TableColumn();
+ decodeEventCountColumn.setPrefWidth(50);
+ decodeEventCountColumn.setText("Events");
+ decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
+
+ TableColumn decoderStateEventCountColumn = new TableColumn();
+ decoderStateEventCountColumn.setPrefWidth(50);
+ decoderStateEventCountColumn.setText("States");
+ decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
+
+ TableColumn channelStartCountColumn = new TableColumn();
+ channelStartCountColumn.setPrefWidth(50);
+ channelStartCountColumn.setText("Starts");
+ channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
+
+ mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
+ decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
+ }
+
+ return mMessagePackageTableView;
+ }
+
+ /**
+ * File selection button
+ * @return button
+ */
+ private Button getSelectFileButton()
+ {
+ if(mSelectFileButton == null)
+ {
+ mSelectFileButton = new Button("Select ...");
+ mSelectFileButton.onActionProperty().set(event -> {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Select P25 Phase 2 .bits Recording");
+ String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
+ if(lastDirectory != null)
+ {
+ File file = new File(lastDirectory);
+ if(file.exists() && file.isDirectory())
+ {
+ fileChooser.setInitialDirectory(file);
+ }
+ }
+ fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
+ final File selected = fileChooser.showOpenDialog(getScene().getWindow());
+
+ if(selected != null)
+ {
+ mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
+ load(selected);
+ }
+ });
+ }
+
+ return mSelectFileButton;
+ }
+
+ /**
+ * Spinny loading icon to show over the message table view
+ */
+ private ProgressIndicator getLoadingIndicator()
+ {
+ if(mLoadingIndicator == null)
+ {
+ mLoadingIndicator = new ProgressIndicator();
+ mLoadingIndicator.setProgress(-1);
+ mLoadingIndicator.setVisible(false);
+ }
+
+ return mLoadingIndicator;
+ }
+
+ /**
+ * Selected file path label.
+ */
+ private Label getSelectedFileLabel()
+ {
+ if(mSelectedFileLabel == null)
+ {
+ mSelectedFileLabel = new Label(" ");
+ }
+
+ return mSelectedFileLabel;
+ }
+
+ /**
+ * Check box to apply filter to show/hide TS0 messages
+ * @return check box control
+ */
+ private CheckBox getShowTS0()
+ {
+ if(mShowTS0 == null)
+ {
+ mShowTS0 = new CheckBox("TS0");
+ mShowTS0.setSelected(true);
+ mShowTS0.setOnAction(event -> updateFilters());
+ }
+
+ return mShowTS0;
+ }
+
+ /**
+ * Check box to apply filter to show/hide TS0 messages
+ * @return check box control
+ */
+ private CheckBox getShowTS1()
+ {
+ if(mShowTS1 == null)
+ {
+ mShowTS1 = new CheckBox("TS1");
+ mShowTS1.setSelected(true);
+ mShowTS1.setOnAction(event -> updateFilters());
+ }
+
+ return mShowTS1;
+ }
+
+ /**
+ * Check box to apply filter to show/hide TS0 messages
+ * @return check box control
+ */
+ private CheckBox getShowTS2()
+ {
+ if(mShowTS2 == null)
+ {
+ mShowTS2 = new CheckBox("TS2");
+ mShowTS2.setSelected(true);
+ mShowTS2.setOnAction(event -> updateFilters());
+ }
+
+ return mShowTS2;
+ }
+
+ /**
+ * Search text filter box
+ * @return text control for entering search text
+ */
+ private TextField getSearchText()
+ {
+ if(mSearchText == null)
+ {
+ mSearchText = new TextField();
+ mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
+ }
+
+ return mSearchText;
+ }
+
+ /**
+ * Text box for find text
+ */
+ private TextField getFindText()
+ {
+ if(mFindText == null)
+ {
+ mFindText = new TextField();
+ mFindText.setOnKeyPressed(event -> {
+ if(event.getCode().equals(KeyCode.ENTER))
+ {
+ getFindButton().fire();
+ }
+ });
+ mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
+ }
+
+ return mFindText;
+ }
+
+ /**
+ * Find button to search for the text in the find text box.
+ * @return button
+ */
+ private Button getFindButton()
+ {
+ if(mFindButton == null)
+ {
+ mFindButton = new Button("Find");
+ mFindButton.setOnAction(event -> find(getFindText().getText()));
+ }
+
+ return mFindButton;
+ }
+
+ /**
+ * Find next button to search for the text in the find text box.
+ * @return button
+ */
+ private Button getFindNextButton()
+ {
+ if(mFindNextButton == null)
+ {
+ mFindNextButton = new Button("Next");
+ mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
+ }
+
+ return mFindNextButton;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java
index dcc293927..8ad5ec688 100644
--- a/src/main/java/io/github/dsheirer/identifier/Form.java
+++ b/src/main/java/io/github/dsheirer/identifier/Form.java
@@ -18,6 +18,8 @@
*/
package io.github.dsheirer.identifier;
+import java.util.EnumSet;
+
/**
* Identifier form. Indicates the type of identifier.
*/
@@ -64,4 +66,21 @@ public enum Form
UNIQUE_ID,
WACN,
ANY;
+
+ Form()
+ {
+ }
+
+ /**
+ * Entity forms that are used to identify entities in P25 call events.
+ */
+ public static EnumSet