diff --git a/.idea/misc.xml b/.idea/misc.xml
index ee9aaf73..669e0e68 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,7 +4,50 @@
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
index 56cb74c6..a899a61b 100644
--- a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
+++ b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 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
- * * *****************************************************************************
+ * 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.alias.id.radio;
@@ -26,14 +23,14 @@ import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
+import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.text.ParseException;
import java.util.EnumMap;
import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Protocol-specific formatting of radio id values and parsing of formatted radio id values.
@@ -47,6 +44,7 @@ public class RadioFormatter
{
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
+ mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}
diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
index 8fb332f9..db7a7ed3 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2020 Dennis Sheirer
- * *
- * * This program is free software: you can redistribute it and/or modify
- * * it under the terms of the GNU General Public License as published by
- * * the Free Software Foundation, either version 3 of the License, or
- * * (at your option) any later version.
- * *
- * * This program is distributed in the hope that it will be useful,
- * * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * * GNU General Public License for more details.
- * *
- * * You should have received a copy of the GNU General Public License
- * * along with this program. If not, see
- * * *****************************************************************************
+ * 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.alias.id.talkgroup;
@@ -50,6 +47,8 @@ public enum TalkgroupFormat
"MPT-1327 valid ranges are 0-127(prefix) and 1-8192(ident) (ie. 000-0001 to 127-8192)"),
NBFM("*****", 1, 0xFFFF, "1 to 65,535",
"NBFM valid value range is 1-65,535"),
+ NXDN("*****", 1, 0xFFFF, "1 to 65,535",
+ "NXDN talkgroup valid range is 1 to 65,535"),
PASSPORT("*****", 1, 0xFFFF, "1 to 65,535",
"Passport valid value range is 1-65,535"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
@@ -139,6 +138,8 @@ public enum TalkgroupFormat
return MPT1327;
case NBFM:
return NBFM;
+ case NXDN:
+ return NXDN;
case PASSPORT:
return PASSPORT;
default:
diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
index f1cdd164..b42ca926 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,7 @@ import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupForm
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
+import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
@@ -59,6 +60,7 @@ public class TalkgroupFormatter
mFormatterMap.put(Protocol.LTR_NET, ltr);
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
+ mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
index e02c07e0..ddb8ff19 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequence.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2022 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonRootName;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.encryption.EncryptionKey;
-import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.util.ArrayList;
import java.util.List;
@@ -288,10 +287,18 @@ public class MBECallSequence
}
else
{
- mVoiceFrames.add(new VoiceFrame(timestamp, frame));
+ add(new VoiceFrame(timestamp, frame));
}
}
+ /**
+ * Adds the voice frame to the sequence
+ */
+ public void add(VoiceFrame voiceFrame)
+ {
+ mVoiceFrames.add(voiceFrame);
+ }
+
/**
* Adds an encrypted audio frame to this call sequence
*
@@ -317,6 +324,6 @@ public class MBECallSequence
*/
public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator)
{
- mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
+ add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
}
}
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java
index 497c7b65..60099f3a 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
-import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.preference.UserPreferences;
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java
index be9b4864..2cb6b4b5 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceReader.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 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
- * * *****************************************************************************
+ * 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.audio.codec.mbe;
@@ -25,7 +22,6 @@ package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
-import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.io.IOException;
import java.nio.file.Files;
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java
similarity index 77%
rename from src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java
rename to src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java
index c7c9d090..957a73f1 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/VoiceFrame.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/VoiceFrame.java
@@ -1,20 +1,23 @@
-/*******************************************************************************
- * sdr-trunk
- * Copyright (C) 2014-2019 Dennis Sheirer
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
- * later version.
+ * This program is 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.module.decode.p25.audio;
+package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
*/
@JsonRootName(value = "frame")
@JsonInclude(JsonInclude.Include.NON_NULL)
-@JsonPropertyOrder({"encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
+@JsonPropertyOrder({"tag", "encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
public class VoiceFrame
{
private long mTimestamp;
@@ -35,6 +38,7 @@ public class VoiceFrame
private Integer mAlgorithm;
private Integer mKeyId;
private String mMessageIndicator;
+ private String mTag;
public VoiceFrame()
{
@@ -70,6 +74,24 @@ public class VoiceFrame
mMessageIndicator = messageIndicator;
}
+ /**
+ * Tag value for the voice frame
+ */
+ @JsonProperty("tag")
+ public String getTag()
+ {
+ return mTag;
+ }
+
+ /**
+ * Sets the optional tag value for the frame
+ * @param tag to set
+ */
+ public void setTag(String tag)
+ {
+ mTag = tag;
+ }
+
/**
* Timestamp the frame was transmitted
*
diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
index 0539f7bd..f181e524 100644
--- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
+++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -306,6 +306,15 @@ public class BinaryMessage extends BitSet
}
public String toString()
+ {
+ return toHexString();
+ }
+
+ /**
+ * Binary representation of the message
+ * @return message in binary 0/1 format
+ */
+ public String toBinaryString()
{
StringBuilder sb = new StringBuilder();
@@ -317,6 +326,10 @@ public class BinaryMessage extends BitSet
return sb.toString();
}
+ /**
+ * Hex string representation of the message
+ * @return message in hex
+ */
public String toHexString()
{
StringBuilder sb = new StringBuilder();
@@ -997,6 +1010,18 @@ public class BinaryMessage extends BitSet
return String.format("%0" + width + "X", getInt(field));
}
+ /**
+ * Returns the long field formatted as a hex value using zero prefixes to pad the hex character count to fully
+ * represent the size (width) of the field.
+ * @param field to parse as hex
+ * @return hex value.
+ */
+ public String getHex(LongField field)
+ {
+ int width = Math.ceilDiv(field.width(), 4);
+ return String.format("%0" + width + "X", getLong(field));
+ }
+
/**
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
@@ -1315,37 +1340,15 @@ public class BinaryMessage extends BitSet
throw new IllegalArgumentException("Message must contain only 0-9 and A-F hexadecimal characters");
}
- int length = hex.length() / 2;
+ BinaryMessage message = new BinaryMessage(hex.length() * 4);
- if(length * 2 < hex.length())
+ for(int x = 0; x < hex.length(); x++)
{
- length++;
+ String rawHex = hex.substring(x, x + 1);
+ message.setInt(Integer.parseInt(rawHex, 16), IntField.length4(x * 4));
}
- BinaryMessage buffer = new BinaryMessage(length * 8);
-
- int bufferOffset = 0;
- for(int x = 0; x < hex.length(); x += 2)
- {
- int endIndex = x + 2;
- if(endIndex > hex.length())
- {
- endIndex--;
- }
-
- String rawHex = hex.substring(x, endIndex);
-
- if(rawHex.length() == 1)
- {
- rawHex = rawHex + "0";
- }
-
- int value = Integer.parseInt(rawHex, 16);
- buffer.setByte(bufferOffset, (byte)(value & 0xFF));
- bufferOffset += 8;
- }
-
- return buffer;
+ return message;
}
/**
diff --git a/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java b/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java
index 3f11a9c6..84412587 100644
--- a/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java
+++ b/src/main/java/io/github/dsheirer/bits/CorrectedBinaryMessage.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,6 +18,7 @@
*/
package io.github.dsheirer.bits;
+import java.nio.ByteOrder;
import java.util.BitSet;
public class CorrectedBinaryMessage extends BinaryMessage
@@ -54,6 +55,45 @@ public class CorrectedBinaryMessage extends BinaryMessage
this.xor(message);
}
+ /**
+ * Creates a new message loaded from the hexadecimal string
+ * @param hex representing the bits of the message
+ * @return new message
+ */
+ public static CorrectedBinaryMessage loadHex(String hex)
+ {
+ return new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
+ }
+
+ /**
+ * Overloads the parent method to return this type.
+ * @param binary message to load
+ * @return loaded message
+ */
+ public static CorrectedBinaryMessage load(String binary)
+ {
+ return new CorrectedBinaryMessage(BinaryMessage.load(binary));
+ }
+
+ /**
+ * Constructs a binary frame from the byte array with length set to the
+ * number of bits contained in the byte array
+ */
+ public static CorrectedBinaryMessage from(byte[] data, ByteOrder byteOrder)
+ {
+ if(byteOrder == ByteOrder.LITTLE_ENDIAN)
+ {
+ return new CorrectedBinaryMessage(from(data));
+ }
+ else
+ {
+ CorrectedBinaryMessage frame = new CorrectedBinaryMessage(data.length * 8);
+ BitSet bitSet = BitSet.valueOf(data);
+ frame.xor(bitSet);
+ return frame;
+ }
+ }
+
@Override
public int getCorrectedBitCount()
{
diff --git a/src/main/java/io/github/dsheirer/bits/IntField.java b/src/main/java/io/github/dsheirer/bits/IntField.java
index 3f6239a2..372fc3cc 100644
--- a/src/main/java/io/github/dsheirer/bits/IntField.java
+++ b/src/main/java/io/github/dsheirer/bits/IntField.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -60,7 +60,27 @@ public record IntField(int start, int end)
}
/**
- * Utility constructor method for a field with one four bits of length.
+ * Utility constructor method for a field with two bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length2(int start)
+ {
+ return new IntField(start, (start + 1));
+ }
+
+ /**
+ * Utility constructor method for a field with three bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length3(int start)
+ {
+ return new IntField(start, (start + 2));
+ }
+
+ /**
+ * Utility constructor method for a field with four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -70,7 +90,17 @@ public record IntField(int start, int end)
}
/**
- * Utility constructor method for a field with one four bits of length.
+ * Utility constructor method for a field with five bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length5(int start)
+ {
+ return new IntField(start, (start + 4));
+ }
+
+ /**
+ * Utility constructor method for a field with six bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -79,6 +109,16 @@ public record IntField(int start, int end)
return new IntField(start, (start + 5));
}
+ /**
+ * Utility constructor method for a field with seven bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length7(int start)
+ {
+ return new IntField(start, (start + 6));
+ }
+
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
@@ -89,6 +129,36 @@ public record IntField(int start, int end)
return new IntField(start, (start + 7));
}
+ /**
+ * Utility constructor method for a field with nine bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length9(int start)
+ {
+ return new IntField(start, (start + 8));
+ }
+
+ /**
+ * Utility constructor method for a field with ten bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length10(int start)
+ {
+ return new IntField(start, (start + 9));
+ }
+
+ /**
+ * Utility constructor method for a field with eleven bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length11(int start)
+ {
+ return new IntField(start, (start + 10));
+ }
+
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
@@ -99,6 +169,26 @@ public record IntField(int start, int end)
return new IntField(start, (start + 11));
}
+ /**
+ * Utility constructor method for a field with 14 bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length14(int start)
+ {
+ return new IntField(start, (start + 13));
+ }
+
+ /**
+ * Utility constructor method for a field with 15 bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static IntField length15(int start)
+ {
+ return new IntField(start, (start + 14));
+ }
+
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
diff --git a/src/main/java/io/github/dsheirer/bits/LongField.java b/src/main/java/io/github/dsheirer/bits/LongField.java
index 9bf09102..f2325995 100644
--- a/src/main/java/io/github/dsheirer/bits/LongField.java
+++ b/src/main/java/io/github/dsheirer/bits/LongField.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -39,6 +39,15 @@ public record LongField(int start, int end)
}
}
+ /**
+ * Width of the field in bit positions
+ * @return width
+ */
+ public int width()
+ {
+ return end() - start() + 1;
+ }
+
/**
* Utility constructor method.
* @param start index (inclusive)
@@ -101,7 +110,17 @@ public record LongField(int start, int end)
}
/**
- * Utility constructor method for a field with two octets of length.
+ * Utility constructor method for a field with 23 bits of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static LongField length23(int start)
+ {
+ return new LongField(start, (start + 22));
+ }
+
+ /**
+ * Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -111,7 +130,7 @@ public record LongField(int start, int end)
}
/**
- * Utility constructor method for a field with three octets of length.
+ * Utility constructor method for a field with four octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -119,4 +138,24 @@ public record LongField(int start, int end)
{
return new LongField(start, (start + 31));
}
+
+ /**
+ * Utility constructor method for a field with seven octets of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static LongField length56(int start)
+ {
+ return new LongField(start, (start + 55));
+ }
+
+ /**
+ * Utility constructor method for a field with eight octets of length.
+ * @param start index (inclusive)
+ * @return constructed bit field.
+ */
+ public static LongField length64(int start)
+ {
+ return new LongField(start, (start + 63));
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
index e7df429b..87c0ba26 100644
--- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -98,6 +98,33 @@ public class FloatCircularBuffer
return values;
}
+ /**
+ * Gets all sample values in reverse order.
+ */
+ public float[] getAllReversed()
+ {
+ int valuePointer = 0;
+ float[] values = new float[mBuffer.length];
+ int bufferPointer = mBufferPointer - 1;
+
+ if(bufferPointer < 0)
+ {
+ bufferPointer = mBuffer.length - 1;
+ }
+
+ while(valuePointer < values.length)
+ {
+ values[valuePointer++] = mBuffer[bufferPointer--];
+
+ if(bufferPointer < 0)
+ {
+ bufferPointer = mBuffer.length - 1;
+ }
+ }
+
+ return values;
+ }
+
/**
* Places the new value into the buffer, overwriting the oldest value
* @param newValue to add to the buffer
diff --git a/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java b/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java
new file mode 100644
index 00000000..d9dee242
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/channel/IBandChannelDescriptor.java
@@ -0,0 +1,40 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+package io.github.dsheirer.channel;
+
+import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
+
+/**
+ * Frequency band channel descriptor
+ */
+public interface IBandChannelDescriptor extends IChannelDescriptor
+{
+
+ /**
+ * Frequency band identifiers (0 - 15)
+ */
+ int[] getFrequencyBandIdentifiers();
+
+ /**
+ * Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies
+ * @param bandIdentifier
+ */
+ void setFrequencyBand(IFrequencyBand bandIdentifier);
+
+}
diff --git a/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java b/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java
index 05ccb6f5..81d11eb0 100644
--- a/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java
+++ b/src/main/java/io/github/dsheirer/channel/IChannelDescriptor.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2019 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,41 +14,29 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
+
package io.github.dsheirer.channel;
-import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
-/**
- * Channel descriptor
- */
public interface IChannelDescriptor
{
/**
* Downlink frequency for the channel
+ *
* @return frequency in hertz
*/
long getDownlinkFrequency();
/**
* Uplink frequency for the channel
+ *
* @return frequency in hertz
*/
long getUplinkFrequency();
- /**
- * Frequency band identifiers (0 - 15)
- */
- int[] getFrequencyBandIdentifiers();
-
- /**
- * Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies
- * @param bandIdentifier
- */
- void setFrequencyBand(IFrequencyBand bandIdentifier);
-
/**
* Indicates if the timeslot count is greater than 1;
*/
diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
index 40fc56ee..633898c6 100644
--- a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
index 05f4da0e..8a68e274 100644
--- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -523,19 +523,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, timeslot));
}
- break;
- case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
- //Rebroadcast frequency error measurements to external listener if we're currently
- //in an active (ie sync locked) state.
- for(int timeslot: mTimeslots)
- {
- if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
- {
- broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
- getChannel().getChannelType().name()));
- return;
- }
- }
break;
}
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
index 5d57868f..abfda065 100644
--- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -392,15 +392,6 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mIdentifierUpdateNotificationProxy.receive(new IdentifierUpdateNotification(
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, 0));
break;
- case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
- //Rebroadcast frequency error measurements to external listener if we're currently
- //in an active (ie sync locked) state.
- if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
- {
- broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
- getChannel().getChannelType().name()));
- }
- break;
}
}
}
@@ -421,9 +412,8 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mSquelchController.setSquelchLock(true);
break;
case REQUEST_CHANGE_CALL_TIMEOUT:
- if(event instanceof ChangeChannelTimeoutEvent)
+ if(event instanceof ChangeChannelTimeoutEvent timeout)
{
- ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
case CONTINUATION:
@@ -471,7 +461,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
* Proxy between the internal identifier collection and the external update notification listener. This proxy
* enables access to internal components to broadcast silent identifier update notifications externally.
*/
- public class IdentifierUpdateNotificationProxy implements Listener
+ public static class IdentifierUpdateNotificationProxy implements Listener
{
private Listener mIdentifierUpdateNotificationListener;
diff --git a/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java
index 5d69c524..d60b90de 100644
--- a/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/TimeslotDecoderState.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@
package io.github.dsheirer.channel.state;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
@@ -89,9 +89,9 @@ public abstract class TimeslotDecoderState extends DecoderState
{
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifier).getValue());
}
- else if(identifier instanceof IChannelDescriptor)
+ else if(identifier instanceof IBandChannelDescriptor)
{
- setCurrentChannel((IChannelDescriptor)identifier);
+ setCurrentChannel((IBandChannelDescriptor)identifier);
}
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
{
diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java
index acac9284..620eb5bf 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,13 +19,13 @@
package io.github.dsheirer.controller.channel.event;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.module.ModuleEventBusMessage;
import io.github.dsheirer.module.decode.event.DecodeEventHistory;
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
-
import java.util.ArrayList;
import java.util.List;
@@ -66,7 +66,7 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage
* @param channelDescriptor that identifies which channel in a multi-channel config to start
* @param identifierCollection to use for the new channel
*/
- public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor,
+ public ChannelStartProcessingRequest(Channel channel, IBandChannelDescriptor channelDescriptor,
IdentifierCollection identifierCollection)
{
this(channel, channelDescriptor, identifierCollection, null);
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
index fc754be6..410b942b 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -963,11 +963,11 @@ public class FilterFactory
* @return filter coefficients.
* @throws FilterDesignException if the requested length is not odd
*/
- public static float[] getSinc(double cutoff, int length, WindowType windowType) throws FilterDesignException
+ public static float[] getSinc(double cutoff, int length, WindowType windowType)
{
if(length % 2 == 0)
{
- throw new FilterDesignException("Sinc filters must be odd-length");
+ throw new IllegalArgumentException("Sinc filters must be odd-length");
}
float[] coefficients = new float[length];
@@ -983,7 +983,46 @@ public class FilterFactory
for(int x = 1; x <= half; x++)
{
double a = piScalor * x;
- double coefficient = scalor * FastMath.sin(a) / a;
+ double coefficient = scalor * (FastMath.sin(a) / a);
+
+ coefficient *= window[half + x];
+ coefficients[half + x] = (float)coefficient;
+ coefficients[half - x] = (float)coefficient;
+ }
+
+ return coefficients;
+ }
+
+ /**
+ * Creates a windowed-sync (Nyquist) filter.
+ *
+ * @param cutoff frequency (0.0 <> 0.5)
+ * @param length of the filter - must be odd-length
+ * @param windowType to use in designing the filter
+ * @return filter coefficients.
+ * @throws FilterDesignException if the requested length is not odd
+ */
+ public static float[] getInverseSync(double cutoff, int length, WindowType windowType)
+ {
+ if(length % 2 == 0)
+ {
+ throw new IllegalArgumentException("Sinc filters must be odd-length");
+ }
+
+ float[] coefficients = new float[length];
+ int half = length / 2;
+
+ float[] window = WindowFactory.getWindow(windowType, length);
+
+ double scalor = 2.0 * cutoff;
+ double piScalor = FastMath.PI * scalor;
+
+ coefficients[half] = (float)(1.0 * scalor * window[half]);
+
+ for(int x = 1; x <= half; x++)
+ {
+ double a = piScalor * x;
+ double coefficient = scalor * (a / FastMath.sin(a));
coefficient *= window[half + x];
coefficients[half + x] = (float)coefficient;
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java
index df51ecf4..3daa92ad 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
index 00ba4aee..44cf02b9 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.buffer.INativeBuffer;
-import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
@@ -73,7 +72,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
private Broadcaster mSourceEventBroadcaster = new Broadcaster<>();
- private INativeBufferProvider mNativeBufferProvider;
+ private TunerController mTunerController;
private List mChannelSources = new CopyOnWriteArrayList<>();
private ChannelCalculator mChannelCalculator;
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
@@ -85,23 +84,20 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
private boolean mRunning = true;
/**
- * Creates a polyphase channel manager instance.
+ * Creates a polyphase channel manager for the tuner controller
*
- * @param nativeBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
- * streams
- * @param frequency of the baseband complex buffer sample stream (ie center frequency)
- * @param sampleRate of the baseband complex buffer sample stream
+ * @param tunerController for a tuner that provides a baseband complex buffer stream.
*/
- public PolyphaseChannelManager(INativeBufferProvider nativeBufferProvider, long frequency, double sampleRate)
+ public PolyphaseChannelManager(TunerController tunerController)
{
- if(nativeBufferProvider == null)
+ if(tunerController == null)
{
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
}
- mNativeBufferProvider = nativeBufferProvider;
+ mTunerController = tunerController;
- int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
+ int channelCount = (int)(tunerController.getSampleRate() / MINIMUM_CHANNEL_BANDWIDTH);
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
if(channelCount % 2 != 0)
@@ -109,21 +105,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
channelCount--;
}
- mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
+ mChannelCalculator = new ChannelCalculator(tunerController.getSampleRate(), channelCount,
+ tunerController.getFrequency(), CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
mBufferDispatcher.setListener(mNativeBufferReceiver);
}
- /**
- * Creates a polyphase channel manager for the tuner controller
- *
- * @param tunerController for a tuner that provides a baseband complex buffer stream.
- */
- public PolyphaseChannelManager(TunerController tunerController)
- {
- this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
- }
-
/**
* Provides a description of the state of this manager.
*/
@@ -201,7 +188,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
try
{
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
- mChannelSourceEventListener, threadName);
+ mChannelSourceEventListener, threadName, mTunerController.getTunerFrequencyErrorManager());
mChannelSources.add(channelSource);
}
@@ -234,7 +221,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
//If this is the first channel, register to start the sample buffers flowing
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
{
- mNativeBufferProvider.addBufferListener(mBufferDispatcher);
+ mTunerController.addBufferListener(mBufferDispatcher);
mPolyphaseChannelizer.start();
mBufferDispatcher.start();
}
@@ -263,7 +250,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
//If this is the last/only channel, deregister to stop the sample buffers
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
{
- mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
+ mTunerController.removeBufferListener(mBufferDispatcher);
mBufferDispatcher.stop();
mPolyphaseChannelizer.stop();
}
@@ -303,6 +290,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
case NOTIFICATION_RECORDING_FILE_LOADED:
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//no-op
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
@@ -456,9 +444,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
- case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
- //Rebroadcast so that the tuner source can process this event
- mSourceEventBroadcaster.broadcast(sourceEvent);
+ case REQUEST_FREQUENCY_CORRECTION:
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
+ //Ignore
break;
default:
mLog.error("Received unrecognized source event from polyphase channel source [" +
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
index 95adc4dd..9503277a 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
+import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
@@ -47,6 +48,8 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
private List mOutputProcessorIndexes = new ArrayList<>();
private double mTunerSampleRate;
private double mTunerCenterFrequency;
+ private long mFrequencyCorrection;
+ private boolean mPendingFrequencyCorrectionUpdate = false;
private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate;
/**
@@ -57,17 +60,27 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
* @param filterManager for access to new or cached synthesis filters
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
* @param threadName for the channel's dispatcher
+ * @param tunerFrequencyErrorManager to connect with the channel frequency error manager
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
*/
- public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
- Listener producerSourceEventListener, String threadName)
+ public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator,
+ SynthesisFilterManager filterManager,
+ Listener producerSourceEventListener, String threadName,
+ TunerFrequencyErrorManager tunerFrequencyErrorManager)
throws IllegalArgumentException
{
- super(producerSourceEventListener, tunerChannel, threadName);
+ super(producerSourceEventListener, tunerChannel, threadName, tunerFrequencyErrorManager);
mChannelSampleRate = channelCalculator.getChannelSampleRate();
doUpdateOutputProcessor(channelCalculator, filterManager);
}
+ @Override
+ public void setFrequencyCorrection(long correction)
+ {
+ mFrequencyCorrection = correction;
+ mPendingFrequencyCorrectionUpdate = true;
+ }
+
/**
* Description of the state or configuration of the output processor.
*/
@@ -280,6 +293,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
doUpdateOutputProcessor(channelCalculator, filterManager);
}
+ //Apply channel decider requested frequency correction
+ if(mPendingFrequencyCorrectionUpdate)
+ {
+ mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
+ mPendingFrequencyCorrectionUpdate = false;
+ }
+
try
{
if(mPolyphaseChannelOutputProcessor != null)
@@ -349,7 +369,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
*/
public long getFrequencyOffset()
{
- return mIndexCenterFrequency - getTunerChannel().getFrequency();
+ return mIndexCenterFrequency - getTunerChannel().getFrequency() + mFrequencyCorrection;
}
@Override
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
index 698a03ab..9a8aabea 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,9 +19,6 @@
package io.github.dsheirer.dsp.filter.design;
-import io.github.dsheirer.dsp.filter.FilterFactory;
-import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
-import io.github.dsheirer.dsp.window.WindowType;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -59,32 +56,68 @@ public class FilterViewer extends Application
*/
private float[] getFilter()
{
- int filterLength = 31;
- WindowType windowType = WindowType.BLACKMAN_HARRIS_7;
+// FIRFilterSpecification specification = FIRFilterSpecification
+// .lowPassBuilder()
+// .sampleRate(12500)
+// .passBandCutoff(2600)
+// .passBandAmplitude(1.0).passBandRipple(0.01)
+// .stopBandAmplitude(0.0).stopBandStart(3125)
+// .stopBandRipple(0.01).build();
+//
+// float[] taps = null;
+//
+// try
+// {
+// taps = FilterFactory.getTaps(specification);
+// }
+// catch(Exception fde) //FilterDesignException
+// {
+// System.out.println("Error");
+// }
- FIRFilterSpecification specification = FIRFilterSpecification
- .lowPassBuilder()
- .sampleRate(12500)
- .passBandCutoff(5200)
- .passBandAmplitude(1.0).passBandRipple(0.01) //.01
- .stopBandAmplitude(0.0).stopBandStart(6500) //6500
- .stopBandRipple(0.01).build();
+ float decimatedSampleRate = 12500f;
+ int symbolLength = 26;
+ float rrcAlpha = 0.2f;
- float[] taps = null;
+// float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / 4800, symbolLength, rrcAlpha);
+// float[] taps = FilterFactory.getSinc(1100d / 12500d, 31, WindowType.BLACKMAN_HARRIS_7);
+// float[] taps = FilterFactory.getInverseSync(1100d/12500d, 31, WindowType.BLACKMAN_HARRIS_7);
- try
- {
- taps = FilterFactory.getTaps(specification);
- }
- catch(Exception fde) //FilterDesignException
- {
- System.out.println("Error");
- }
-
- // float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType);
-// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
-
-// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
+ float[] taps = { +0.031462429f, +0.031747267f, +0.030401148f, +0.027362877f,
+ +0.022653298f, +0.016379869f, +0.008737200f, +0.000003302f,
+ -0.009468531f, -0.019262057f, -0.028914291f, -0.037935027f,
+ -0.045828927f, -0.052119261f, -0.056372283f, -0.058221106f,
+ -0.057387924f, -0.053703443f, -0.047122444f, -0.037734535f,
+ -0.025769308f, -0.011595336f, +0.004287292f, +0.021260954f,
+ +0.038610717f, +0.055550276f, +0.071252765f, +0.084885375f,
+ +0.095646450f, +0.102803611f, +0.105731303f, +0.103946126f,
+ +0.097138329f, +0.085197939f, +0.068234131f, +0.046586711f,
+ +0.020828821f, -0.008239664f, -0.039608255f, -0.072081234f,
+ -0.104311776f, -0.134843790f, -0.162160200f, -0.184736015f,
+ -0.201094346f, -0.209863285f, -0.209831516f, -0.200000470f,
+ -0.179630919f, -0.148282051f, -0.105841323f, -0.052543664f,
+ +0.011020985f, +0.083912428f, +0.164857408f, +0.252278939f,
+ +0.344336996f, +0.438979335f, +0.534000832f, +0.627109358f,
+ +0.715995947f, +0.798406824f, +0.872214756f, +0.935487176f,
+ +0.986548646f, +1.024035395f, +1.046939951f, +1.054644241f,
+ +1.046939951f, +1.024035395f, +0.986548646f, +0.935487176f,
+ +0.872214756f, +0.798406824f, +0.715995947f, +0.627109358f,
+ +0.534000832f, +0.438979335f, +0.344336996f, +0.252278939f,
+ +0.164857408f, +0.083912428f, +0.011020985f, -0.052543664f,
+ -0.105841323f, -0.148282051f, -0.179630919f, -0.200000470f,
+ -0.209831516f, -0.209863285f, -0.201094346f, -0.184736015f,
+ -0.162160200f, -0.134843790f, -0.104311776f, -0.072081234f,
+ -0.039608255f, -0.008239664f, +0.020828821f, +0.046586711f,
+ +0.068234131f, +0.085197939f, +0.097138329f, +0.103946126f,
+ +0.105731303f, +0.102803611f, +0.095646450f, +0.084885375f,
+ +0.071252765f, +0.055550276f, +0.038610717f, +0.021260954f,
+ +0.004287292f, -0.011595336f, -0.025769308f, -0.037734535f,
+ -0.047122444f, -0.053703443f, -0.057387924f, -0.058221106f,
+ -0.056372283f, -0.052119261f, -0.045828927f, -0.037935027f,
+ -0.028914291f, -0.019262057f, -0.009468531f, +0.000003302f,
+ +0.008737200f, +0.016379869f, +0.022653298f, +0.027362877f,
+ +0.030401148f, +0.031747267f, +0.031462429f
+ };
if(taps == null)
{
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
index 656a9bb4..dadd0bc4 100644
--- a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -52,6 +52,44 @@ public enum Dibit
mPhase = phase;
}
+ private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
+
+ /**
+ * Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
+ * @param sample in radians.
+ * @return symbol decision.
+ */
+ public static Dibit fromSample(float sample)
+ {
+ if(sample > 0)
+ {
+ return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
+ }
+ else
+ {
+ return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
+ }
+ }
+
+ /**
+ * Calculates the error from the sample by first mapping it to a symbol and the error is the difference from
+ * the ideal phase of that symbol.
+ * @param sample to process
+ * @return error from ideal
+ */
+ public static float getError(float sample)
+ {
+ if(sample > 0)
+ {
+ return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3.mPhase - sample : Dibit.D00_PLUS_1.mPhase - sample;
+ }
+ else
+ {
+ return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3.mPhase - sample : Dibit.D10_MINUS_1.mPhase - sample;
+ }
+ }
+
+
/**
* Binary value of the symbol
*/
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java
index 9ad6403a..daf1438f 100644
--- a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -73,6 +73,18 @@ public class DibitDelayLine
}
}
+ /**
+ * Inserts the dibit array into the buffer
+ * @param dibits to fill
+ */
+ public void fill(Dibit[] dibits)
+ {
+ for(Dibit dibit: dibits)
+ {
+ insert(dibit);
+ }
+ }
+
/**
* Adjusts the pointer to correct a dibit stuff/delete.
* @param offset to adjust.
@@ -110,7 +122,7 @@ public class DibitDelayLine
public void log()
{
StringBuilder sb = new StringBuilder();
-
+ sb.append("Pointer [").append(mPointer).append("] ");
int pointer = mPointer;
for(int i = 0; i < mDelayLine.length; i++)
{
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java
index 159d7a6b..606e243a 100644
--- a/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitToByteBufferAssembler.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2022 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ package io.github.dsheirer.dsp.symbol;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
+import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,8 +30,7 @@ import org.slf4j.LoggerFactory;
*/
public class DibitToByteBufferAssembler implements Listener, IByteBufferProvider
{
- private final static Logger mLog = LoggerFactory.getLogger(DibitToByteBufferAssembler.class);
-
+ private final static Logger LOGGER = LoggerFactory.getLogger(DibitToByteBufferAssembler.class);
private ByteBuffer mCurrentBuffer;
private int mBufferSize;
private byte mCurrentByte;
@@ -53,14 +53,21 @@ public class DibitToByteBufferAssembler implements Listener, IByteBufferP
* the buffer pointer to zero so that new dibits can be loaded.
*/
private void getNextBuffer()
+ {
+ closeAndBroadcast();
+ mCurrentBuffer = ByteBuffer.allocate(mBufferSize);
+ }
+
+ /**
+ * Closes the current buffer and broadcasts it to an optionally registered listener
+ */
+ private void closeAndBroadcast()
{
if(mCurrentBuffer != null && mBufferListener != null)
{
mCurrentBuffer.flip();
mBufferListener.receive(mCurrentBuffer);
}
-
- mCurrentBuffer = ByteBuffer.allocate(mBufferSize);
}
@Override
@@ -88,17 +95,33 @@ public class DibitToByteBufferAssembler implements Listener, IByteBufferP
if(mDibitCount >= 4)
{
- mCurrentBuffer.put(mCurrentByte);
- mCurrentByte = 0x00;
- mDibitCount = 0;
-
if(!mCurrentBuffer.hasRemaining())
{
getNextBuffer();
}
+
+ try
+ {
+ mCurrentBuffer.put(mCurrentByte);
+ }
+ catch (BufferOverflowException e)
+ {
+ //This shouldn't happen.
+ LOGGER.info("Detected buffer overflow in dibit buffer assembler - continuing");
+ }
+ mCurrentByte = 0x00;
+ mDibitCount = 0;
}
}
+ /**
+ * Flush any buffered bits to the registered listener.
+ */
+ public void flush()
+ {
+ closeAndBroadcast();
+ }
+
/**
* Registers the listener to receive fully assembled byte buffers from this assembler.
*/
diff --git a/src/main/java/io/github/dsheirer/edac/CRCUtil.java b/src/main/java/io/github/dsheirer/edac/CRCUtil.java
index 572c1218..e9bafd94 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-2024 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -139,7 +139,7 @@ public class CRCUtil
BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
polySet.load(i, crcSize + 1, polynomial);
message.xor(polySet);
- System.out.println(message.toString());
+// System.out.println(message.toString());
}
return message;
@@ -183,29 +183,29 @@ public class CRCUtil
if(!integerArray)
{
- element.append("l");
+ element.append("L");
}
element.append(", ");
- if(row.length() + element.length() <= 80)
+ if(row.length() + element.length() <= 120)
{
- row.append(element.toString());
+ row.append(element);
}
else
{
- sb.append(row.toString());
+ sb.append(row);
sb.append("\n");
row = new StringBuilder();
row.append(" ");
- row.append(element.toString());
+ row.append(element);
}
}
if(row.length() > 4)
{
- sb.append(row.toString());
+ sb.append(row);
sb.append("\n");
}
diff --git a/src/main/java/io/github/dsheirer/filter/FilterEditor.java b/src/main/java/io/github/dsheirer/filter/FilterEditor.java
index 2d5ffec6..cb169ecc 100644
--- a/src/main/java/io/github/dsheirer/filter/FilterEditor.java
+++ b/src/main/java/io/github/dsheirer/filter/FilterEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,6 +53,7 @@ public class FilterEditor extends JFrame
setLayout(new MigLayout("", "[grow,fill]", "[grow,fill][]"));
mEditorPanel = new FilterEditorPanel<>(filterSet);
JScrollPane scroller = new JScrollPane(mEditorPanel);
+ scroller.getVerticalScrollBar().setUnitIncrement(16);
scroller.setViewportView(mEditorPanel);
add(scroller, "wrap");
JButton close = new JButton("Close");
diff --git a/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java
index 0e8d1e4a..65374ef3 100644
--- a/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java
+++ b/src/main/java/io/github/dsheirer/gui/channel/ChannelSpectrumPanel.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,6 +33,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule;
import io.github.dsheirer.settings.SettingsManager;
+import io.github.dsheirer.source.ChannelFrequencyCorrectionStatusNotification;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource;
@@ -62,6 +63,7 @@ import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
+import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
@@ -75,6 +77,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener {
+ String message = "Not Implemented";
+
+ if(mFeedbackDecoder != null)
+ {
+ message = mFeedbackDecoder.getEqualizerSettings();
+ }
+
+ JOptionPane.showMessageDialog(this, message);
+ });
+
+ mBButton = new JButton("Reset");
+ mBButton.setToolTipText("Resets the equalizer to default tap values");
+ mBButton.addActionListener(e -> {
+ if(mFeedbackDecoder != null)
+ {
+ mFeedbackDecoder.resetEqualizer();
+ }
+ });
+ labelPanel.add(mAButton);
+ labelPanel.add(mBButton);
mNoiseFloorSpinnerModel = new SpinnerNumberModel(18, 8, 36, 1);
mNoiseFloorSpinnerModel.addChangeListener(e -> {
@@ -120,8 +173,8 @@ public class ChannelSpectrumPanel extends JPanel implements Listener {
@@ -229,11 +282,15 @@ public class ChannelSpectrumPanel extends JPanel implements Listener {
- mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(carrierOffsetFrequency));
- mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(true);
- });
-
- mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(carrierOffsetFrequency);
- }
-
private void broadcast(SourceEvent sourceEvent)
{
if(mProcessingChain != null)
@@ -327,8 +368,14 @@ public class ChannelSpectrumPanel extends JPanel implements Listener {
+ mTunerPPMLabel.setText(LABEL_PREFIX_PPM + PPM_FORMAT.format(status.getTunerPPM()));
+ mTunerPPMLabel.setEnabled(true);
+
+ if(status.isAutoPPM())
+ {
+ mTunerCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_TUNER, status.getTunerCorrection()));
+ }
+ else
+ {
+ mTunerCorrectionLabel.setText(LABEL_PREFIX_TUNER + " disabled");
+ }
+ mTunerCorrectionLabel.setEnabled(true);
+ mChannelCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_CHANNEL, status.getChannelCorrection()));
+ mChannelCorrectionLabel.setEnabled(true);
+ mDecoderCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_DECODER, status.getDecoderCorrection()));
+ mDecoderCorrectionLabel.setEnabled(true);
+
+ //We se the offset cursor line to negative so that it aligns with where the signal is located
+ //instead of the correction that's needed to bring the signal to zero.
+ mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(-status.getDecoderCorrection());
+ });
+ }
}
+//TODO: is this still needed?
mSignalPowerView.receive(sourceEvent);
}
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
index 70b5a9cf..bc96a1a6 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -744,7 +744,6 @@ public class AliasItemEditor extends Editor
dmrMenu.getItems().add(new AddRadioIdRangeItem(Protocol.DMR));
dmrMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
-
Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC);
fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC));
fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.FLEETSYNC));
@@ -766,6 +765,13 @@ public class AliasItemEditor extends Editor
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddDcsItem());
+ Menu nxdnMenu = new ProtocolMenu(Protocol.NXDN);
+ nxdnMenu.getItems().add(new AddTalkgroupItem(Protocol.NXDN));
+ nxdnMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NXDN));
+ nxdnMenu.getItems().add(new AddRadioIdItem(Protocol.NXDN));
+ nxdnMenu.getItems().add(new AddRadioIdRangeItem(Protocol.NXDN));
+ nxdnMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
+
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT));
@@ -779,7 +785,7 @@ public class AliasItemEditor extends Editor
lojackMenu.getItems().add(new AddLojackItem());
mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
- nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
+ nbfmMenu, nxdnMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
}
return mAddIdentifierButton;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
index 30ac1cd1..4f319d91 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
+import java.util.ArrayList;
+import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* Editor for talkgroup alias identifiers
*/
@@ -244,6 +243,10 @@ public class RadioIdEditor extends IdentifierEditor
"Format: 0 - 16777215"));
mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
"Format: 0 - FFFFFF"));
+ mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
+ "Format: 0 - 65,535"));
+ mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
+ "Format: 0 - FFFF"));
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
"Format: 0 - 8388607"));
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java
index b0d7e4b9..18224491 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
+import java.util.ArrayList;
+import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* Editor for radio ID range alias identifiers
*/
@@ -293,6 +292,10 @@ public class RadioIdRangeEditor extends IdentifierEditor
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF"));
+ details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
+ new IntegerFormatter(0,0xFFFF),"Format: 0 - 65,535"));
+ details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
+ new IntegerFormatter(0,0xFFFF),"Format: 0 - FFFF"));
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
index efdc4b6f..a8c8f69d 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -263,6 +263,10 @@ public class TalkgroupEditor extends IdentifierEditor
"Format: 1 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
"Format: 1 - FFFF"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
+ "Format: 1 - 65,535"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
+ "Format: 1 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java
index 4f86bd7b..a9826866 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -298,6 +298,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
new HexFormatter(0,65535), "Format: 0 - FFFF"));
+ details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215),
+ new IntegerFormatter(1,16777215), "Format: 1 - 16777215"));
+ details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF),
+ new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF"));
details.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
@@ -314,6 +318,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor
new IntegerFormatter(0,65535), "Format 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format 0 - FFFF"));
+ details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
+ new IntegerFormatter(1,0xFFFF),"Format: 1 - 65,535"));
+ details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
+ new IntegerFormatter(1,0xFFFF),"Format: 1 - FFFF"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
@@ -324,10 +332,6 @@ public class TalkgroupRangeEditor extends IdentifierEditor
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
- details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215),
- new IntegerFormatter(1,16777215), "Format: 1 - 16777215"));
- details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF),
- new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF"));
return details;
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
index c71a77f4..322db01e 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -55,14 +55,16 @@ public class ChannelConfigurationEditorFactory
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case DMR:
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
- case NBFM:
- return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR_NET:
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR:
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case MPT1327:
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
+ case NBFM:
+ return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
+ case NXDN:
+ return new NXDNConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case PASSPORT:
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case P25_PHASE1:
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java
new file mode 100644
index 00000000..f735e180
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/NXDNConfigurationEditor.java
@@ -0,0 +1,718 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.playlist.channel;
+
+import io.github.dsheirer.gui.control.IntegerTextField;
+import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
+import io.github.dsheirer.gui.playlist.record.RecordConfigurationEditor;
+import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
+import io.github.dsheirer.gui.playlist.source.FrequencyField;
+import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
+import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
+import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.channel.ObservableChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.module.log.EventLogType;
+import io.github.dsheirer.module.log.config.EventLogConfiguration;
+import io.github.dsheirer.playlist.PlaylistManager;
+import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.record.RecorderType;
+import io.github.dsheirer.record.config.RecordConfiguration;
+import io.github.dsheirer.source.config.SourceConfiguration;
+import io.github.dsheirer.source.tuner.manager.TunerManager;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.control.ToggleSwitch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * NXDN channel configuration editor
+ */
+public class NXDNConfigurationEditor extends ChannelConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(NXDNConfigurationEditor.class);
+ private TitledPane mDecoderPane;
+ private TitledPane mEventLogPane;
+ private TitledPane mRecordPane;
+ private TitledPane mSourcePane;
+ private SourceConfigurationEditor mSourceConfigurationEditor;
+ private EventLogConfigurationEditor mEventLogConfigurationEditor;
+ private RecordConfigurationEditor mRecordConfigurationEditor;
+ private Spinner mTrafficChannelPoolSizeSpinner;
+ private TableView mChannelMapTable;
+ private IntegerTextField mChannelField;
+ private FrequencyField mDownlinkFrequencyField;
+ private Button mAddTimeslotFrequencyButton;
+ private Button mDeleteButton;
+ private Spinner mChannelRotationDelaySpinner;
+ private SegmentedButton mTransmissionModeButton;
+ private ComboBox mEncodingComboBox;
+ private ToggleSwitch mIgnoreDataCallsToggle;
+ private ToggleSwitch mIgnoreEncryptedCallsToggle;
+
+ /**
+ * Constructs an instance
+ * @param playlistManager for playlists
+ * @param tunerManager for tuners
+ * @param userPreferences for preferences
+ */
+ public NXDNConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
+ UserPreferences userPreferences, IFilterProcessor filterProcessor)
+ {
+ super(playlistManager, tunerManager, userPreferences, filterProcessor);
+ getTitledPanesBox().getChildren().add(getSourcePane());
+ getTitledPanesBox().getChildren().add(getDecoderPane());
+ getTitledPanesBox().getChildren().add(getEventLogPane());
+ getTitledPanesBox().getChildren().add(getRecordPane());
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.NXDN;
+ }
+
+ private TitledPane getSourcePane()
+ {
+ if(mSourcePane == null)
+ {
+ mSourcePane = new TitledPane("Source", getSourceConfigurationEditor());
+ mSourcePane.setExpanded(true);
+ }
+
+ return mSourcePane;
+ }
+
+ private TitledPane getDecoderPane()
+ {
+ if(mDecoderPane == null)
+ {
+ mDecoderPane = new TitledPane();
+ mDecoderPane.setText("Decoder: NXDN");
+ mDecoderPane.setExpanded(true);
+
+ GridPane gridPane = new GridPane();
+ gridPane.setPadding(new Insets(10,10,10,10));
+ gridPane.setHgap(10);
+ gridPane.setVgap(10);
+
+ int row = 0;
+
+ Label mode = new Label("Mode:");
+ GridPane.setHalignment(mode, HPos.RIGHT);
+ GridPane.setConstraints(mode, 0, row);
+ gridPane.getChildren().add(mode);
+
+ GridPane.setConstraints(getTransmissionModeButton(), 1, row);
+ gridPane.getChildren().add(getTransmissionModeButton());
+
+ Label encoding = new Label("Talker Alias Encoding:");
+ GridPane.setHalignment(encoding, HPos.RIGHT);
+ GridPane.setConstraints(encoding, 2, row);
+ gridPane.getChildren().add(encoding);
+
+ GridPane.setConstraints(getEncodingComboBox(), 3, row);
+ gridPane.getChildren().add(getEncodingComboBox());
+
+ Label poolSizeLabel = new Label("Max Traffic Channels");
+ GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
+ GridPane.setConstraints(poolSizeLabel, 4, row);
+ gridPane.getChildren().add(poolSizeLabel);
+
+ GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 5, row);
+ gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
+
+ Label ignoreLabel = new Label("Ignore:");
+ GridPane.setHalignment(ignoreLabel, HPos.RIGHT);
+ GridPane.setConstraints(ignoreLabel, 0, ++row);
+ gridPane.getChildren().add(ignoreLabel);
+
+ GridPane.setConstraints(getIgnoreDataCallsToggle(), 1, row);
+ gridPane.getChildren().add(getIgnoreDataCallsToggle());
+
+ GridPane.setConstraints(getIgnoreEncryptedCallsToggle(), 2, row, 2, 1);
+ gridPane.getChildren().add(getIgnoreEncryptedCallsToggle());
+
+ Label channelMapTableLabel = new Label("Optional Channel Number (LCN) to Frequency Map for Channel-Mode systems. Ignore for Direct Frequency Access (DFA) systems.");
+ GridPane.setHalignment(channelMapTableLabel, HPos.LEFT);
+ GridPane.setConstraints(channelMapTableLabel, 0, ++row, 6, 1);
+ gridPane.getChildren().add(channelMapTableLabel);
+
+ GridPane.setConstraints(getChannelMapTable(), 0, ++row, 6, 3);
+ gridPane.getChildren().add(getChannelMapTable());
+
+ VBox buttonsBox = new VBox();
+ buttonsBox.setAlignment(Pos.CENTER);
+ buttonsBox.setSpacing(10);
+ buttonsBox.getChildren().addAll(getAddTimeslotFrequencyButton(), getDeleteButton());
+
+ GridPane.setConstraints(buttonsBox, 6, row, 1, 3);
+ gridPane.getChildren().addAll(buttonsBox);
+
+ row += 3;
+
+ HBox editorBox = new HBox();
+ editorBox.setAlignment(Pos.CENTER_LEFT);
+ editorBox.setSpacing(5);
+
+ Label lcnLabel = new Label("Channel Number");
+ editorBox.getChildren().addAll(lcnLabel, getChannelField());
+
+ Label downlinkLabel = new Label("Downlink Frequency (MHz)");
+ downlinkLabel.setPadding(new Insets(0,0,0,5));
+ editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
+
+ GridPane.setConstraints(editorBox, 0, row, 4, 1);
+ gridPane.getChildren().add(editorBox);
+
+ mDecoderPane.setContent(gridPane);
+ }
+
+ return mDecoderPane;
+ }
+
+ private TitledPane getEventLogPane()
+ {
+ if(mEventLogPane == null)
+ {
+ mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor());
+ mEventLogPane.setExpanded(false);
+ }
+
+ return mEventLogPane;
+ }
+
+ private TitledPane getRecordPane()
+ {
+ if(mRecordPane == null)
+ {
+ mRecordPane = new TitledPane();
+ mRecordPane.setText("Recording");
+ mRecordPane.setExpanded(false);
+
+ Label notice = new Label("Note: use aliases to control call audio recording");
+ notice.setPadding(new Insets(10, 10, 0, 10));
+
+ VBox vBox = new VBox();
+ vBox.getChildren().addAll(getRecordConfigurationEditor(), notice);
+
+ mRecordPane.setContent(vBox);
+ }
+
+ return mRecordPane;
+ }
+
+ private SourceConfigurationEditor getSourceConfigurationEditor()
+ {
+ if(mSourceConfigurationEditor == null)
+ {
+ mSourceConfigurationEditor = new FrequencyEditor(mTunerManager,
+ DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MINIMUM_MS,
+ DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MAXIMUM_MS,
+ DecodeConfigDMR.CHANNEL_ROTATION_DELAY_DEFAULT_MS);
+
+ //Add a listener so that we can push change notifications up to this editor
+ mSourceConfigurationEditor.modifiedProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mSourceConfigurationEditor;
+ }
+
+ private EventLogConfigurationEditor getEventLogConfigurationEditor()
+ {
+ if(mEventLogConfigurationEditor == null)
+ {
+ List types = new ArrayList<>();
+ types.add(EventLogType.CALL_EVENT);
+ types.add(EventLogType.DECODED_MESSAGE);
+ types.add(EventLogType.TRAFFIC_CALL_EVENT);
+ types.add(EventLogType.TRAFFIC_DECODED_MESSAGE);
+
+ mEventLogConfigurationEditor = new EventLogConfigurationEditor(types);
+ mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5));
+ mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mEventLogConfigurationEditor;
+ }
+
+ /**
+ * Talker alias encoding combo box
+ * @return encoding combo box.
+ */
+ private ComboBox getEncodingComboBox()
+ {
+ if(mEncodingComboBox == null)
+ {
+ mEncodingComboBox = new ComboBox<>(FXCollections.observableArrayList(Arrays.stream(Encoding.values()).toList()));
+ mEncodingComboBox.getSelectionModel().select(0);
+ mEncodingComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ mEncodingComboBox.setTooltip(new Tooltip("Talker Alias Encoding: UTF-8:Latin/English, BIG5:Taiwan"));
+ }
+
+ return mEncodingComboBox;
+ }
+
+ private TableView getChannelMapTable()
+ {
+ if(mChannelMapTable == null)
+ {
+ mChannelMapTable = new TableView<>(FXCollections.observableArrayList(ObservableChannelFrequency.extractor()));
+ mChannelMapTable.setPrefHeight(100.0);
+
+ TableColumn channelColumn = new TableColumn<>("Channel");
+ channelColumn.setPrefWidth(100);
+ channelColumn.setCellValueFactory(cellData -> cellData.getValue().channelProperty());
+ mChannelMapTable.getColumns().add(channelColumn);
+ mChannelMapTable.getSortOrder().add(channelColumn);
+
+ TableColumn downlinkColumn = new TableColumn<>("Downlink Frequency (MHz)");
+ downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().downlinkProperty());
+ downlinkColumn.setPrefWidth(200);
+ mChannelMapTable.getColumns().add(downlinkColumn);
+
+ mChannelMapTable.getSelectionModel().selectedItemProperty()
+ .addListener((observable, oldValue, newValue) -> setChannelFrequency(newValue));
+ }
+
+ return mChannelMapTable;
+ }
+
+ private SegmentedButton getTransmissionModeButton()
+ {
+ if(mTransmissionModeButton == null)
+ {
+ mTransmissionModeButton = new SegmentedButton();
+ mTransmissionModeButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+ mTransmissionModeButton.setDisable(true);
+
+ ToggleButton tb4800 = new ToggleButton(TransmissionMode.M4800.getLabel());
+ tb4800.setUserData(TransmissionMode.M4800);
+ mTransmissionModeButton.getButtons().add(tb4800);
+
+ ToggleButton tb9600 = new ToggleButton(TransmissionMode.M9600.getLabel());
+ tb9600.setUserData(TransmissionMode.M9600);
+ mTransmissionModeButton.getButtons().add(tb9600);
+
+ ToggleButton tbTypeD = new ToggleButton(TransmissionMode.TYPE_D.getLabel());
+ tbTypeD.setUserData(TransmissionMode.TYPE_D);
+ mTransmissionModeButton.getButtons().add(tbTypeD);
+
+ mTransmissionModeButton.getToggleGroup().selectToggle(tb4800);
+
+ mTransmissionModeButton.getToggleGroup().selectedToggleProperty()
+ .addListener((observable, oldValue, newValue) -> {
+ //Only trigger change detection when both values are not null and not equal to each other
+ if(oldValue != null && newValue != null && !oldValue.equals(newValue))
+ {
+ //Only set modified if the toggle changed from one to the other
+ modifiedProperty().set(true);
+ }
+ });
+ }
+
+ return mTransmissionModeButton;
+ }
+
+
+
+ /**
+ * Sets the specified timeslot frequency into the editor
+ */
+ private void setChannelFrequency(ObservableChannelFrequency channelFrequency)
+ {
+ //Preserve the current modified flag state since setting values in the editor will change it.
+ boolean modified = modifiedProperty().get();
+
+ getChannelField().setDisable(channelFrequency == null);
+ getDownlinkFrequencyField().setDisable(channelFrequency == null);
+ getDeleteButton().setDisable(channelFrequency == null);
+
+ if(channelFrequency != null)
+ {
+ getChannelField().set(channelFrequency.channelProperty().get());
+ getDownlinkFrequencyField().set(channelFrequency.downlinkProperty().get());
+ }
+ else
+ {
+ getChannelField().set(0);
+ getDownlinkFrequencyField().set(0);
+ }
+
+ modifiedProperty().set(modified);
+ }
+
+ private Button getAddTimeslotFrequencyButton()
+ {
+ if(mAddTimeslotFrequencyButton == null)
+ {
+ mAddTimeslotFrequencyButton = new Button("Add");
+ mAddTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE);
+ mAddTimeslotFrequencyButton.setOnAction(event -> addChannelFrequency());
+ }
+
+ return mAddTimeslotFrequencyButton;
+ }
+
+ /**
+ * Adds a new channel frequency entry
+ */
+ private void addChannelFrequency()
+ {
+ ObservableChannelFrequency ocf = new ObservableChannelFrequency();
+ getChannelMapTable().getItems().add(ocf);
+ getChannelMapTable().scrollTo(ocf);
+ getChannelMapTable().getSelectionModel().select(ocf);
+ modifiedProperty().set(true);
+ }
+
+ private Button getDeleteButton()
+ {
+ if(mDeleteButton == null)
+ {
+ mDeleteButton = new Button("Delete");
+ mDeleteButton.setDisable(true);
+ mDeleteButton.setMaxWidth(Double.MAX_VALUE);
+ mDeleteButton.setOnAction(event -> {
+ ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ getChannelMapTable().getItems().remove(selected);
+ modifiedProperty().set(true);
+ }
+ });
+ }
+
+ return mDeleteButton;
+ }
+
+ private IntegerTextField getChannelField()
+ {
+ if(mChannelField == null)
+ {
+ mChannelField = new IntegerTextField();
+ mChannelField.setDisable(true);
+ mChannelField.setPrefWidth(65);
+ mChannelField.textProperty().addListener((observable, oldValue, newValue) -> {
+ ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ Integer value = mChannelField.get();
+
+ if(value != null)
+ {
+ selected.channelProperty().set(value);
+ }
+ }
+
+ modifiedProperty().set(true);
+ });
+ }
+
+ return mChannelField;
+ }
+
+ private FrequencyField getDownlinkFrequencyField()
+ {
+ if(mDownlinkFrequencyField == null)
+ {
+ mDownlinkFrequencyField = new FrequencyField();
+ mDownlinkFrequencyField.setDisable(true);
+ mDownlinkFrequencyField.textProperty().addListener(new ChangeListener()
+ {
+ @Override
+ public void changed(ObservableValue extends String> observable, String oldValue, String newValue)
+ {
+ ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ selected.downlinkProperty().set(mDownlinkFrequencyField.get());
+ }
+
+ modifiedProperty().set(true);
+ }
+ });
+ }
+
+ return mDownlinkFrequencyField;
+ }
+
+ 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;
+ }
+
+ /**
+ * Toggle switch for ignore data calls.
+ */
+ private ToggleSwitch getIgnoreDataCallsToggle()
+ {
+ if(mIgnoreDataCallsToggle == null)
+ {
+ mIgnoreDataCallsToggle = new ToggleSwitch("Data Calls");
+ mIgnoreDataCallsToggle.setDisable(true);
+ mIgnoreDataCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true));
+ }
+
+ return mIgnoreDataCallsToggle;
+ }
+
+ /**
+ * Toggle switch for ignore encrypted calls.
+ */
+ private ToggleSwitch getIgnoreEncryptedCallsToggle()
+ {
+ if(mIgnoreEncryptedCallsToggle == null)
+ {
+ mIgnoreEncryptedCallsToggle = new ToggleSwitch("Encrypted Calls");
+ mIgnoreEncryptedCallsToggle.setDisable(true);
+ mIgnoreEncryptedCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true));
+ }
+
+ return mIgnoreEncryptedCallsToggle;
+ }
+
+ /**
+ * Channel rotation monitor delay value. This dictates how long the decoder will remain on each frequency before
+ * rotating to the next frequency in the list
+ * @return spinner
+ */
+ private Spinner getChannelRotationDelaySpinner()
+ {
+ if(mChannelRotationDelaySpinner == null)
+ {
+ mChannelRotationDelaySpinner = new Spinner();
+ mChannelRotationDelaySpinner.setDisable(true);
+ mChannelRotationDelaySpinner.setTooltip(
+ new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency"));
+ mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+ SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(200, 2000, 200, 50);
+ mChannelRotationDelaySpinner.setValueFactory(svf);
+ mChannelRotationDelaySpinner.getValueFactory().valueProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mChannelRotationDelaySpinner;
+ }
+
+ private RecordConfigurationEditor getRecordConfigurationEditor()
+ {
+ if(mRecordConfigurationEditor == null)
+ {
+ List types = new ArrayList<>();
+ 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()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mRecordConfigurationEditor;
+ }
+
+ @Override
+ protected void setDecoderConfiguration(DecodeConfiguration config)
+ {
+ getTransmissionModeButton().setDisable(config == null);
+ getTrafficChannelPoolSizeSpinner().setDisable(config == null);
+ getChannelMapTable().getItems().clear();
+ getChannelMapTable().setDisable(config == null);
+ getAddTimeslotFrequencyButton().setDisable(config == null);
+ getDeleteButton().setDisable(true);
+ getChannelField().set(0);
+ getChannelField().setDisable(true);
+ getDownlinkFrequencyField().set(0);
+ getDownlinkFrequencyField().setDisable(true);
+ getChannelRotationDelaySpinner().setDisable(config == null);
+ getIgnoreDataCallsToggle().setDisable(config == null);
+ getIgnoreEncryptedCallsToggle().setDisable(config == null);
+
+ if(config instanceof DecodeConfigNXDN configNXDN)
+ {
+ for(ToggleButton toggle: getTransmissionModeButton().getButtons())
+ {
+ if(toggle.getUserData() == configNXDN.getTransmissionMode())
+ {
+ toggle.setSelected(true);
+ }
+ }
+
+ getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(configNXDN.getTrafficChannelPoolSize());
+
+ for(ChannelFrequency channelFrequency: configNXDN.getChannelMap())
+ {
+ getChannelMapTable().getItems().add(new ObservableChannelFrequency(channelFrequency));
+ }
+
+ getIgnoreDataCallsToggle().setSelected(configNXDN.isIgnoreDataCalls());
+ getIgnoreEncryptedCallsToggle().setSelected(configNXDN.isIgnoreEncryptedCalls());
+ }
+ else
+ {
+ getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0);
+ getChannelRotationDelaySpinner().getValueFactory().setValue(200);
+ getIgnoreDataCallsToggle().setSelected(false);
+ getIgnoreEncryptedCallsToggle().setSelected(false);
+ }
+ }
+
+ @Override
+ protected void saveDecoderConfiguration()
+ {
+ DecodeConfigNXDN config = null;
+
+ if(getItem().getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
+ {
+ config = configNXDN;
+ }
+ else
+ {
+ config = new DecodeConfigNXDN(TransmissionMode.M4800);
+ }
+
+ TransmissionMode selected = (TransmissionMode)getTransmissionModeButton().getToggleGroup().selectedToggleProperty().get().getUserData();
+ config.setTransmissionMode(selected);
+
+ config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
+
+ List channelFrequencies = new ArrayList<>();
+
+ for(ObservableChannelFrequency ocf: getChannelMapTable().getItems())
+ {
+ channelFrequencies.add(ocf.getChannel());
+ }
+
+ config.setChannelMap(channelFrequencies);
+ config.setIgnoreDataCalls(getIgnoreDataCallsToggle().isSelected());
+ config.setIgnoreEncryptedCalls(getIgnoreEncryptedCallsToggle().isSelected());
+ getItem().setDecodeConfiguration(config);
+ }
+
+ @Override
+ protected void setEventLogConfiguration(EventLogConfiguration config)
+ {
+ getEventLogConfigurationEditor().setItem(config);
+ }
+
+ @Override
+ protected void saveEventLogConfiguration()
+ {
+ getEventLogConfigurationEditor().save();
+
+ if(getEventLogConfigurationEditor().getItem().getLoggers().isEmpty())
+ {
+ getItem().setEventLogConfiguration(null);
+ }
+ else
+ {
+ getItem().setEventLogConfiguration(getEventLogConfigurationEditor().getItem());
+ }
+ }
+
+ @Override
+ protected void setAuxDecoderConfiguration(AuxDecodeConfiguration config)
+ {
+ //no-op
+ }
+
+ @Override
+ protected void saveAuxDecoderConfiguration()
+ {
+ //no-op
+ }
+
+ @Override
+ protected void setRecordConfiguration(RecordConfiguration config)
+ {
+ getRecordConfigurationEditor().setDisable(config == null);
+ getRecordConfigurationEditor().setItem(config);
+ }
+
+ @Override
+ protected void saveRecordConfiguration()
+ {
+ getRecordConfigurationEditor().save();
+ RecordConfiguration config = getRecordConfigurationEditor().getItem();
+ getItem().setRecordConfiguration(config);
+ }
+
+ @Override
+ protected void setSourceConfiguration(SourceConfiguration config)
+ {
+ getSourceConfigurationEditor().setSourceConfiguration(config);
+ }
+
+ @Override
+ protected void saveSourceConfiguration()
+ {
+ getSourceConfigurationEditor().save();
+ SourceConfiguration sourceConfiguration = getSourceConfigurationEditor().getSourceConfiguration();
+ getItem().setSourceConfiguration(sourceConfiguration);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
index 4d987322..ef7ad35d 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2022 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,7 +35,7 @@ public enum ModeDecoderType
DSTAR("d-star", null),
FM("fm", DecoderType.NBFM),
FMN("fmn", DecoderType.NBFM),
- NXDN("nxdn", null),
+ NXDN("nxdn", DecoderType.NXDN),
TELM("telm", null),
USB("usb", null),
LSB("lsb", null),
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 8ef9655b..535f15a7 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-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,10 @@ import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.identifier.talkgroup.UnknownTalkgroupIdentifier;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
+import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkgroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
import io.github.dsheirer.preference.UserPreferences;
@@ -146,10 +149,14 @@ public class RadioReferenceDecoder
{
case APCO25:
return APCO25Talkgroup.create(value);
+ case DMR:
+ return DMRTalkgroup.create(value);
case LTR:
return LTRTalkgroup.create(value);
case MPT1327:
return MPT1327Talkgroup.createTo(value);
+ case NXDN:
+ return NXDNTalkgroupIdentifier.createTo(value);
case PASSPORT:
return PassportTalkgroup.create(value);
default:
@@ -385,6 +392,55 @@ public class RadioReferenceDecoder
return Collections.emptyList();
}
+ /**
+ * Indicates if the specified site is an NXDN site that has site frequencies that can be converted
+ * to channel frequency map
+ * @param systemInformation to determine the system type
+ * @param site containing site frequencies
+ */
+ public boolean hasChannelMap(SystemInformation systemInformation, Site site)
+ {
+ Type type = getType(systemInformation);
+ return type != null && type.getName().contains("NXDN") && !site.getSiteFrequencies().isEmpty();
+ }
+
+ /**
+ * Creates a channel frequency map listing from the radio reference site's list of site frequencies for the site
+ * @param systemInformation to detect the system type (NXDN)
+ * @param site containing 1 or more site frequencies
+ * @return list of timeslot frequency mappings or an empty list.
+ */
+ public List getChannelMap(SystemInformation systemInformation, Site site)
+ {
+ if(hasChannelMap(systemInformation, site))
+ {
+ List frequencies = new ArrayList<>();
+
+ for(SiteFrequency siteFrequency: site.getSiteFrequencies())
+ {
+ int lcn = siteFrequency.getLogicalChannelNumber();
+
+ if(siteFrequency.getChannelId() != null)
+ {
+ try
+ {
+ lcn = Integer.parseInt(siteFrequency.getChannelId());
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the LSN from the channel ID value
+ }
+ }
+
+ frequencies.add(new ChannelFrequency(lcn, (long)(siteFrequency.getFrequency() * 1E6), 0));
+ }
+
+ return frequencies;
+ }
+
+ return Collections.emptyList();
+ }
+
/**
* Identifies the sdrtrunk protocol used by the system.
* @param system to identify
@@ -430,6 +486,7 @@ public class RadioReferenceDecoder
}
break;
case "NXDN":
+ return Protocol.NXDN;
case "EDACS":
case "TETRA":
case "Midland CMS":
@@ -440,6 +497,8 @@ public class RadioReferenceDecoder
default:
}
+// mLog.info("Type:" + type + " Flavor:" + flavor + " Voice:" + voice);
+
return Protocol.UNKNOWN;
}
@@ -503,6 +562,7 @@ public class RadioReferenceDecoder
}
break;
case "NXDN":
+ return DecoderType.NXDN;
case "EDACS":
case "TETRA":
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 30e01809..5ce2641e 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-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,9 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
+import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
import io.github.dsheirer.module.decode.p25.phase1.Modulation;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
@@ -90,7 +93,6 @@ 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;
@@ -250,6 +252,39 @@ public class SiteEditor extends GridPane
.getTimeslotFrequencies(systemInformation, site);
dmr.setTimeslotMap(timeslotFrequencies);
return dmr;
+ case NXDN:
+ Flavor flavor = mRadioReferenceDecoder.getFlavor(systemInformation);
+
+ TransmissionMode mode = TransmissionMode.M4800;
+
+ /**
+ * 9600 bps Flavors
+ * ----------------------------
+ * 29 - NEXEDGE 9600
+ * 45 - Conventional Networked
+ *
+ * 4800 bps Flavors
+ * ---------------------------
+ * 11 - Narrowband Networked
+ * 42 - NEXEDGE 4800
+ * 44 - Icom IDAS Type C
+ *
+ * Type-D (also 4800 bps) Flavors
+ * ---------------------------
+ * 36 - Icom IDAS Type D
+ * 47 - Kenwood Type D
+ */
+ mode = switch(flavor.getName())
+ {
+ case "NEXEDGE 9600", "Conventional Networked" -> TransmissionMode.M9600;
+ case "Icom IDAS Type D", "Kenwood Type D" -> TransmissionMode.TYPE_D;
+ default -> TransmissionMode.M4800;
+ };
+
+ DecodeConfigNXDN nxdn = new DecodeConfigNXDN(mode);
+ List channelMap = mRadioReferenceDecoder.getChannelMap(systemInformation, site);
+ nxdn.setChannelMap(channelMap);
+ return nxdn;
case P25_PHASE1:
DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType);
@@ -399,7 +434,7 @@ public class SiteEditor extends GridPane
getCreateChannelConfigurationButton().setVisible(true);
getGoToChannelEditorCheckBox().setVisible(true);
getSystemTextField().setText(system.getName());
- getSiteTextField().setText(mCurrentSite.getCountyName());
+ getSiteTextField().setText(mCurrentSite.getDescription());
getNameTextField().setText("Control");
if(!siteFrequencies.isEmpty())
diff --git a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java
index d633c93c..894ec418 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2022 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -115,6 +115,7 @@ public class TunerPreferenceEditor extends HBox
Label label = new Label("Please restart the application for this change to take effect");
label.setWrapText(true);
Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setHeaderText("Restart Required");
alert.getDialogPane().setContent(label);
alert.initOwner(((Node)getChannelizerTypeChoiceBox()).getScene().getWindow());
alert.show();
@@ -183,6 +184,7 @@ public class TunerPreferenceEditor extends HBox
Label label = new Label("Please restart the application for this change to take effect");
label.setWrapText(true);
Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setHeaderText("Restart Required");
alert.getDialogPane().setContent(label);
alert.initOwner(((Node)getRspDuoTunerModeChoiceBox()).getScene().getWindow());
alert.show();
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java
index 076097eb..af22efc8 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -468,6 +468,10 @@ public class DmrViewer extends VBox
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
+ FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
+ FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
+ fileChooser.getExtensionFilters().addAll(f1, f2);
+ fileChooser.setSelectedExtensionFilter(f1);
fileChooser.setTitle("Select DMR .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
@@ -478,7 +482,6 @@ public class DmrViewer extends VBox
fileChooser.setInitialDirectory(file);
}
}
- fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
index ec49879b..ee10ad27 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,14 +38,35 @@ public class MessagePackage
private List mDecodeEvents = new ArrayList<>();
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
private AudioSegment mAudioSegment;
+ private int mBitCounter;
+ private int mElapsed;
/**
* Constructs an instance
* @param message for this instance
+ * @param bitCounter at the time this instance was created
*/
- public MessagePackage(IMessage message)
+ public MessagePackage(IMessage message, int bitCounter, int elapsed)
{
mMessage = message;
+ mBitCounter = bitCounter;
+ mElapsed = elapsed;
+ }
+
+ /**
+ * Bit counter value for this package
+ */
+ public int getBitCounter()
+ {
+ return mBitCounter;
+ }
+
+ /**
+ * Elapsed bit count from last package
+ */
+ public int getElapsed()
+ {
+ return mElapsed;
}
/**
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
index b4c131d1..42bf6c3c 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,6 +34,8 @@ import io.github.dsheirer.module.decode.event.IDecodeEvent;
public class MessagePackager
{
private MessagePackage mMessagePackage;
+ private int mBitCounter;
+ private int mLastBitCount;
/**
* Constructs an instance
@@ -42,6 +44,15 @@ public class MessagePackager
{
}
+ /**
+ * Updates the current bit count
+ * @param bitCount
+ */
+ public void updateBitCount(int bitCount)
+ {
+ mBitCounter = bitCount;
+ }
+
/**
* Adds an audio segment.
* @param audioSegment to add
@@ -61,7 +72,9 @@ public class MessagePackager
*/
public void add(IMessage message)
{
- mMessagePackage = new MessagePackage(message);
+ int elapsed = mBitCounter - mLastBitCount;
+ mMessagePackage = new MessagePackage(message, mBitCounter, elapsed);
+ mLastBitCount = mBitCounter;
}
/**
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
index c7ff473e..206054eb 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/MessageRecordingViewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory;
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
- * Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
+ * Supported Protocols: DMR, NXDN, and APCO25 Phase 1 and Phase 2.
*/
public class MessageRecordingViewer extends VBox
{
@@ -50,6 +50,7 @@ public class MessageRecordingViewer extends VBox
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
+ private int mTabCounterNxdn = 1;
private int mTabCounterP25P1 = 1;
private int mTabCounterP25P2 = 1;
private UserPreferences mUserPreferences = new UserPreferences();
@@ -77,6 +78,12 @@ public class MessageRecordingViewer extends VBox
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
+ MenuItem nxdnMenuItem = new MenuItem("NXDN");
+ nxdnMenuItem.onActionProperty().set(event -> {
+ Tab tab = new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences));
+ getTabPane().getTabs().add(tab);
+ getTabPane().getSelectionModel().select(tab);
+ });
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
p25p1MenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences));
@@ -89,7 +96,7 @@ public class MessageRecordingViewer extends VBox
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
- createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
+ createNewViewerMenu.getItems().addAll(dmrMenuItem, nxdnMenuItem, p25p1MenuItem, p25p2MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
@@ -110,6 +117,7 @@ public class MessageRecordingViewer extends VBox
mTabPane = new TabPane();
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
+ mTabPane.getTabs().add(new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences)));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)));
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
}
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
new file mode 100644
index 00000000..75403c85
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
@@ -0,0 +1,762 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer;
+
+import com.google.common.eventbus.EventBus;
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.identifier.IdentifierUpdateNotification;
+import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
+import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState;
+import io.github.dsheirer.module.decode.nxdn.NXDNMessageFramer;
+import io.github.dsheirer.module.decode.nxdn.NXDNMessageProcessor;
+import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader;
+import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.record.binary.BinaryReader;
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.util.ThreadPool;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+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.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.Label;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.control.SelectionMode;
+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;
+
+/**
+ * NXDN Viewer
+ */
+public class NxdnViewer extends VBox
+{
+ private static final Logger mLog = LoggerFactory.getLogger(NxdnViewer.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.nxdn";
+ private static final String LAST_SELECTED_FILE = "last.selected.file.nxdn";
+ private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
+ private Preferences mPreferences = Preferences.userNodeForPackage(NxdnViewer.class);
+ private Button mSelectFileButton;
+ private Label mSelectedFileLabel;
+ 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();
+ private UserPreferences mUserPreferences;
+
+ public NxdnViewer(UserPreferences userPreferences)
+ {
+ mUserPreferences = userPreferences;
+ setPadding(new Insets(5));
+ setSpacing(5);
+
+ HBox fileBox = new HBox();
+ fileBox.setMaxWidth(Double.MAX_VALUE);
+ fileBox.setAlignment(Pos.CENTER_LEFT);
+ fileBox.setSpacing(5);
+ HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
+ HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
+ getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
+
+ fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
+
+ 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);
+
+ filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
+ getSearchText());
+
+ VBox.setVgrow(fileBox, Priority.NEVER);
+ VBox.setVgrow(filterBox, Priority.NEVER);
+ VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
+ VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
+
+ getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
+
+ }
+
+ /**
+ * 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 DMR data.
+ */
+ private void load(File file)
+ {
+ if(file != null && file.exists())
+ {
+ mLoadedFile.set(file.toString());
+ mMessagePackages.clear();
+ getLoadingIndicator().setVisible(true);
+ getSelectedFileLabel().setText("Loading ...");
+
+ ThreadPool.CACHED.submit(() -> {
+ DecodeConfigNXDN config = new DecodeConfigNXDN();
+ List messagePackages = new ArrayList<>();
+ NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config);
+
+ Broadcaster messageBroadcaster = new Broadcaster<>();
+ messageProcessor.setMessageListener(messageBroadcaster);
+
+ Channel empty = new Channel("Empty");
+ TransmissionMode transmissionMode = mLoadedFile.get().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600;
+ config.setTransmissionMode(transmissionMode);
+ empty.setDecodeConfiguration(new DecodeConfigNXDN());
+ NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode);
+
+ MessagePackager messagePackager = new MessagePackager();
+
+ //Setup a temporary event bus to capture channel start processing requests
+ EventBus eventBus = new EventBus("debug");
+ eventBus.register(messagePackager);
+
+ NXDNTrafficChannelManager trafficChannelManager = new NXDNTrafficChannelManager(empty);
+ trafficChannelManager.setInterModuleEventBus(eventBus);
+
+ //Register to receive events
+ trafficChannelManager.addDecodeEventListener(messagePackager::add);
+ NXDNDecoderState decoderState = new NXDNDecoderState(empty, trafficChannelManager);
+
+ Broadcaster decoderStateEventBroadcaster = new Broadcaster<>();
+ decoderState.setDecoderStateListener(decoderStateEventBroadcaster);
+
+ decoderStateEventBroadcaster.addListener(messagePackager::add);
+ decoderState.addDecodeEventListener(messagePackager::add);
+ 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, 0));
+ }
+
+ messageProcessor.setMessageListener(message -> {
+ //Add the initial message to the packager so that it can be combined with any decoder state events.
+ if(message.toString().contains("FROM:166"))
+ {
+ int a = 0;
+ }
+ messagePackager.add(message);
+ decoderState.receive(message);
+
+ //Collect the packaged message with events
+ messagePackages.add(messagePackager.getMessageWithEvents());
+ });
+
+ try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
+ {
+ int bitCount = 0;
+
+ while(reader.hasNext())
+ {
+ ByteBuffer buffer = reader.next();
+ for(byte value : buffer.array())
+ {
+ for(int x = 0; x <= 3; x++)
+ {
+ messageFramer.processWithHardSyncDetection(Dibit.parse(value, x));
+ bitCount += 2;
+ messagePackager.updateBitCount(bitCount);
+ }
+ }
+ }
+ }
+ catch(Exception ioe)
+ {
+ ioe.printStackTrace();
+ }
+
+// audioModule.stop();
+// decoderState.stop();
+// singleChannelState.stop();
+
+ Platform.runLater(() -> {
+ getLoadingIndicator().setVisible(false);
+ getSelectedFileLabel().setText(file.getName());
+ 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
+ */
+ private void updateFilters()
+ {
+ String filterText = getSearchText().getText();
+
+ if(filterText != null && !filterText.isEmpty())
+ {
+ Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
+ mFilteredMessagePackages.setPredicate(textPredicate);
+ }
+ else
+ {
+ mFilteredMessagePackages.setPredicate(null);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * List view control with messages
+ */
+ private TableView getMessagePackageTableView()
+ {
+ if(mMessagePackageTableView == null)
+ {
+ mMessagePackageTableView = new TableView<>();
+ mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+ 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 bitCountColumn = new TableColumn();
+ bitCountColumn.setPrefWidth(90);
+ bitCountColumn.setText("Bit Counter");
+ bitCountColumn.setCellValueFactory(new PropertyValueFactory<>("bitCounter"));
+
+ TableColumn elapsedColumn = new TableColumn();
+ elapsedColumn.setPrefWidth(70);
+ elapsedColumn.setText("Elapsed");
+ elapsedColumn.setCellValueFactory(new PropertyValueFactory<>("elapsed"));
+
+ 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"));
+
+ TableColumn audioSegmentCountColumn = new TableColumn();
+ audioSegmentCountColumn.setPrefWidth(50);
+ audioSegmentCountColumn.setText("Audio");
+ audioSegmentCountColumn.setCellValueFactory(new PropertyValueFactory<>("audioSegmentCount"));
+
+ mMessagePackageTableView.getColumns().addAll(timestampColumn, bitCountColumn, elapsedColumn, validColumn,
+ timeslotColumn, messageColumn, decodeEventCountColumn, decoderStateEventCountColumn,
+ channelStartCountColumn, audioSegmentCountColumn);
+ }
+
+ 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 NXDN .bits Recording");
+ FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
+ FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
+ fileChooser.getExtensionFilters().addAll(f1, f2);
+ fileChooser.setSelectedExtensionFilter(f1);
+ String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
+ if(lastDirectory != null)
+ {
+ File file = new File(lastDirectory);
+ if(file.exists() && file.isDirectory())
+ {
+ fileChooser.setInitialDirectory(file);
+
+ String lastFile = mPreferences.get(LAST_SELECTED_FILE, null);
+
+ if(lastFile != null)
+ {
+ fileChooser.initialFileNameProperty().set(lastFile);
+// fileChooser.setInitialFileName(lastFile);
+ }
+ }
+ }
+ final File selected = fileChooser.showOpenDialog(getScene().getWindow());
+
+ if(selected != null)
+ {
+ mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
+ mPreferences.put(LAST_SELECTED_FILE, selected.getName());
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ public static class A implements Listener
+ {
+ private Path mOutput;
+ private int mTarget;
+
+ public A(Path output, int target)
+ {
+ mOutput = output;
+ mTarget = target;
+ }
+
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message instanceof GPS gps)
+ {
+ System.out.println(gps);
+
+ IPacketHeader header = gps.getPacketSequence().getHeader();
+
+ int id = header.getSource().getValue();
+
+ if(mTarget != 0 && id != mTarget)
+ {
+ return;
+ }
+
+ try
+ {
+ Files.writeString(mOutput, gps + "\n", StandardOpenOption.APPEND);
+ }
+ catch(IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ static void main() throws IOException
+ {
+ Path directory = Paths.get("/home/denny/SDRTrunk/recordings");
+ int target = 0;
+ Path output = directory.resolve(target == 0 ? "GPS.txt" : "GPS_" + target + ".txt");
+ A a = new A(output, target);
+
+ if(Files.exists(output))
+ {
+ Files.delete(output);
+ }
+
+ try
+ {
+ output.toFile().createNewFile();
+
+ File[] files = directory.toFile().listFiles();
+
+ if(files == null)
+ {
+ return;
+ }
+
+ List fileList = Arrays.asList(files);
+ Collections.sort(fileList);
+ for(File file: fileList)
+ {
+ if(file.getName().endsWith(".bits"))
+ {
+ if(file != null && file.exists())
+ {
+ DecodeConfigNXDN config = new DecodeConfigNXDN();
+ NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config);
+
+ Channel empty = new Channel("Empty");
+ TransmissionMode transmissionMode = file.getName().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600;
+ config.setTransmissionMode(transmissionMode);
+ empty.setDecodeConfiguration(new DecodeConfigNXDN());
+ NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode);
+ messageProcessor.setMessageListener(a);
+
+ try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
+ {
+ while(reader.hasNext())
+ {
+ ByteBuffer buffer = reader.next();
+ for(byte value : buffer.array())
+ {
+ for(int x = 0; x <= 3; x++)
+ {
+ messageFramer.processWithHardSyncDetection(Dibit.parse(value, x));
+ }
+ }
+ }
+ }
+ catch(Exception ioe)
+ {
+ ioe.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ catch(IOException ioe)
+ {
+ ioe.printStackTrace();
+ }
+ }
+}
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 061d5aa4..775efe18 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-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -508,6 +508,10 @@ public class P25P1Viewer extends VBox
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
+ FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
+ FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
+ fileChooser.getExtensionFilters().addAll(f1, f2);
+ fileChooser.setSelectedExtensionFilter(f1);
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
@@ -518,7 +522,6 @@ public class P25P1Viewer extends VBox
fileChooser.setInitialDirectory(file);
}
}
- fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != 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
index 4166ad43..d31f86c3 100644
--- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
+++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -594,6 +594,10 @@ public class P25P2Viewer extends VBox
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
+ FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
+ FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
+ fileChooser.getExtensionFilters().addAll(f1, f2);
+ fileChooser.setSelectedExtensionFilter(f1);
fileChooser.setTitle("Select P25 Phase 2 .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
@@ -604,7 +608,6 @@ public class P25P2Viewer extends VBox
fileChooser.setInitialDirectory(file);
}
}
- fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java
new file mode 100644
index 00000000..2a518474
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncResultsViewer.java
@@ -0,0 +1,123 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer.sync;
+
+import io.github.dsheirer.util.SwingUtils;
+import java.util.concurrent.CountDownLatch;
+import javafx.application.Platform;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.JFrame;
+
+/**
+ * Utility for viewing sync detection results
+ */
+public class FMSyncResultsViewer implements ISyncResultsListener
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(FMSyncResultsViewer.class.getName());
+ private ISyncResultsListener mListener;
+
+ public FMSyncResultsViewer()
+ {
+ initUI();
+ }
+
+ /**
+ * View sync detection results.
+ * @param symbols that were detected
+ * @param sync that was detected
+ * @param samples for the symbols
+ * @param syncIntervals symbol timing interval pointers into the I/Q sample arrays
+ * @param label to display to the user
+ * @param release latch to release via the UI when done viewing these results
+ */
+ @Override
+ public void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, float equalizerBalance,
+ float equalizerGain, String label, CountDownLatch release)
+ {
+ if(mListener != null)
+ {
+ mListener.receive(symbols, samples, sync, syncIntervals, equalizerBalance, equalizerGain, label, release);
+ }
+ }
+
+ @Override
+ public void symbol(float symbol)
+ {
+ if(mListener != null)
+ {
+ mListener.symbol(symbol);
+ }
+ }
+
+ /**
+ * Initializes the viewer UI
+ */
+ private void initUI()
+ {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ final JFXPanel fxPanel = new JFXPanel();
+ Platform.runLater(() -> {
+ Platform.setImplicitExit(false);
+ FMSyncVisualizer syncVisualizer = new FMSyncVisualizer();
+ mListener = syncVisualizer;
+
+ Scene scene = new Scene(syncVisualizer, 1400, 1000);
+
+// URL resource = getClass().getResource("/sdrtrunk_style.css");
+//
+// if(resource != null)
+// {
+// scene.getStylesheets().add(resource.toExternalForm());
+// }
+// else
+// {
+// LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
+// }
+
+ fxPanel.setScene(scene);
+ fxPanel.setVisible(true);
+
+ JFrame frame = new JFrame();
+ frame.setTitle("Sync Results Viewer");
+ frame.setContentPane(fxPanel);
+ frame.setSize(1400, 1400);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setLocationRelativeTo(null);
+ SwingUtils.run(() -> {
+ frame.setVisible(true);
+ latch.countDown();
+ });
+ });
+
+ try
+ {
+ latch.await();
+ }
+ catch(InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java
new file mode 100644
index 00000000..db235e57
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java
@@ -0,0 +1,220 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.viewer.sync;
+
+import java.util.concurrent.CountDownLatch;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.ScatterChart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+/**
+ * JavaFX viewer application for visualizing sample buffers and detected sync patterns
+ */
+public class FMSyncVisualizer extends VBox implements ISyncResultsListener
+{
+ private static final int SYMBOL_CHART_SERIES_COUNT = 2000;
+ private static final float SYMBOL = (float) (Math.PI / 4.0);
+ private final NumberAxis mConstellationI = new NumberAxis();
+ private final NumberAxis mConstellationQ = new NumberAxis();
+ private final NumberAxis mSampleTiming = new NumberAxis();
+ private final NumberAxis mSamplePhase = new NumberAxis(-4.0, 4.0, .1);
+ private final NumberAxis mSymbolTiming = new NumberAxis();
+ private final NumberAxis mSymbolPhase = new NumberAxis(-Math.PI, Math.PI, Math.PI / 2);
+ private final XYChart.Series mSymbolSeries = new XYChart.Series<>();
+ private final XYChart.Series mSymbolSeriesMarker = new XYChart.Series<>();
+ private final ScatterChart mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
+ private final XYChart.Series mConstellationSymbols = new XYChart.Series<>();
+ private static final int CONSTELLATION_SYMBOL_COUNT = 50;
+ private int mConstellationSymbolPointer = 0;
+
+ private final LineChart mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
+ private final ScatterChart mSymbolChart = new ScatterChart<>(mSymbolTiming, mSymbolPhase);
+
+ private final NumberAxis mEqualizerHistoryAxis = new NumberAxis();
+ private final NumberAxis mEqualizerValueAxis = new NumberAxis(-14.0, 14.0, .25);
+ private final LineChart mEqualizerChart = new LineChart<>(mEqualizerHistoryAxis, mEqualizerValueAxis);
+ private int mEqualizerPointer = 0;
+ private int mEqualizerHistorySize = 40;
+ private float[] mEqualizerBalanceHistory = new float[mEqualizerHistorySize];
+ private float[] mEqualizerGainHistory = new float[mEqualizerHistorySize];
+
+ private final Button mReleaseButton = new Button("Next Sync");
+ private final Label mPatternLabel = new Label();
+ private CountDownLatch mRelease;
+ private int mSymbolPointer = 0;
+
+ /**
+ * Constructs an instance
+ */
+ public FMSyncVisualizer()
+ {
+ setPadding(new Insets(5));
+ mConstellationI.setLabel("Constellation - Inphase");
+ mConstellationQ.setLabel("Quadrature");
+ mSampleTiming.setLabel("Sample Timing");
+ mSamplePhase.setLabel("Sample Phase (+/- PI)");
+
+ mReleaseButton.setOnAction(e -> {
+ if(mRelease != null)
+ {
+ mRelease.countDown();
+ mReleaseButton.setDisable(true);
+ }
+ });
+
+ mConstellationChart.setPrefSize(400, 400);
+ mConstellationChart.setAnimated(false);
+ XYChart.Series constellationIdeal = new XYChart.Series<>();
+ constellationIdeal.setName("Ideal");
+ float PI_4 = (float) (Math.PI / 4);
+ constellationIdeal.getData().add(new XYChart.Data<>(PI_4, PI_4));
+ constellationIdeal.getData().add(new XYChart.Data<>(PI_4, -PI_4));
+ constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, PI_4));
+ constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, -PI_4));
+ mConstellationChart.getData().add(constellationIdeal);
+ mConstellationSymbols.setName("Symbols");
+
+ for(int x = 0; x < CONSTELLATION_SYMBOL_COUNT; x++)
+ {
+ mConstellationSymbols.getData().add(new XYChart.Data<>(0, 0));
+ }
+
+ mConstellationChart.getData().add(mConstellationSymbols);
+
+ mSampleChart.setAnimated(false);
+ mSymbolChart.setAnimated(false);
+ mSymbolChart.setId("symbol-chart");
+ mEqualizerChart.setAnimated(false);
+
+ mEqualizerValueAxis.setLabel("Value");
+ mEqualizerHistoryAxis.setLabel("Equalizer");
+
+ HBox hbox = new HBox();
+ hbox.getChildren().add(mConstellationChart);
+ HBox.setHgrow(mEqualizerChart, Priority.ALWAYS);
+ hbox.getChildren().add(mEqualizerChart);
+ // VBox.setVgrow(hbox, Priority.ALWAYS);
+
+ VBox.setVgrow(mSampleChart, Priority.ALWAYS);
+ VBox.setVgrow(mSymbolChart, Priority.ALWAYS);
+
+ HBox buttons = new HBox();
+ buttons.setAlignment(Pos.BASELINE_LEFT);
+ buttons.setSpacing(10);
+ buttons.getChildren().addAll(mReleaseButton, mPatternLabel);
+ VBox.setVgrow(buttons, Priority.NEVER);
+
+ //Load the symbol chart with symbols
+ mSymbolSeries.setName("Symbols");
+ for(int x = 0; x < SYMBOL_CHART_SERIES_COUNT; x++)
+ {
+ mSymbolSeries.getData().add(new XYChart.Data<>(x, 0));
+ }
+ mSymbolChart.getData().add(mSymbolSeries);
+ float pi_4 = (float)Math.PI / 4f;
+ mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 3f * pi_4));
+ mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, pi_4));
+ mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 0));
+ mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -pi_4));
+ mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -3 * pi_4));
+ mSymbolChart.getData().add(mSymbolSeriesMarker);
+ getChildren().addAll(buttons, hbox, mSampleChart, mSymbolChart);
+ }
+
+ @Override
+ public void symbol(float symbol)
+ {
+ for(XYChart.Data data : mSymbolSeriesMarker.getData())
+ {
+ data.setXValue(mSymbolPointer);
+ }
+ mSymbolSeries.getData().get(mSymbolPointer++).setYValue(symbol);
+ mSymbolPointer %= SYMBOL_CHART_SERIES_COUNT;
+
+ XYChart.Data element = mConstellationSymbols.getData().get(mConstellationSymbolPointer);
+ element.setXValue(Math.cos(symbol));
+ element.setYValue(Math.sin(symbol));
+ mConstellationSymbolPointer++;
+ mConstellationSymbolPointer %= CONSTELLATION_SYMBOL_COUNT;
+ }
+
+ @Override
+ public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, float equalizerBalance, float equalizerGain, String label, CountDownLatch latch)
+ {
+ mRelease = latch;
+ mReleaseButton.setDisable(latch == null);
+
+ mEqualizerBalanceHistory[mEqualizerPointer] = equalizerBalance;
+ mEqualizerGainHistory[mEqualizerPointer++] = (equalizerGain - 1.0f);
+ mEqualizerPointer %= mEqualizerHistorySize;
+ //Clear the next/upcoming values to give visual indication of where we're at.
+ mEqualizerBalanceHistory[mEqualizerPointer] = 0f;
+ mEqualizerGainHistory[mEqualizerPointer] = 0f;
+
+ Platform.runLater(() -> {
+ XYChart.Series sampleSeries = new XYChart.Series<>();
+ sampleSeries.setName("Demodulated Samples");
+
+ for(int x = 0; x < samples.length; x++)
+ {
+ sampleSeries.getData().add(new XYChart.Data<>(x, samples[x]));
+ }
+
+ mSampleChart.getData().clear();
+ mSampleChart.getData().add(sampleSeries);
+
+ XYChart.Series patternSeries = new XYChart.Series<>();
+ patternSeries.setName("Ideal Sync Pattern");
+
+ for(int x = 0; x < sync.length; x++)
+ {
+ patternSeries.getData().add(new XYChart.Data<>(syncIntervals[x], sync[x]));
+ }
+
+ mSampleChart.getData().add(patternSeries);
+
+ mPatternLabel.setText(label);
+ mEqualizerChart.getData().clear();
+
+ XYChart.Series balanceSeries = new XYChart.Series<>();
+ balanceSeries.setName("Balance");
+ XYChart.Series gainSeries = new XYChart.Series<>();
+ gainSeries.setName("Gain (Value minus 1.0)");
+
+ for(int x = 0; x < mEqualizerHistorySize; x++)
+ {
+ balanceSeries.getData().add(new XYChart.Data<>(x, mEqualizerBalanceHistory[x]));
+ gainSeries.getData().add(new XYChart.Data<>(x, mEqualizerGainHistory[x]));
+ }
+
+ mEqualizerChart.getData().add(balanceSeries);
+ mEqualizerChart.getData().add(gainSeries);
+ });
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java
index 8ad5ec68..47dce496 100644
--- a/src/main/java/io/github/dsheirer/identifier/Form.java
+++ b/src/main/java/io/github/dsheirer/identifier/Form.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,6 +30,7 @@ public enum Form
ARS_USER,
ARS_PASSWORD,
CALL_PROGRESS_TONE,
+ CATEGORY,
CHANNEL,
CHANNEL_DESCRIPTOR,
CHANNEL_NAME,
@@ -49,6 +50,7 @@ public enum Form
NETWORK_ACCESS_CODE,
PATCH_GROUP,
RADIO,
+ RAN,
RF_SUBSYSTEM,
SCRAMBLE_PARAMETERS,
SHORT_DATA_MESSAGE,
diff --git a/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java b/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java
index e9aae49c..38e82153 100644
--- a/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java
+++ b/src/main/java/io/github/dsheirer/identifier/configuration/ChannelDescriptorConfigurationIdentifier.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2018 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,12 +14,12 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.identifier.configuration;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
@@ -31,12 +30,12 @@ import io.github.dsheirer.protocol.Protocol;
* Configuration identifier that identifies a channel descriptor. This is primarily used to broadcast the channel
* descriptor for a traffic channel so that it is aware of what channel number and frequency it is decoding.
*/
-public class ChannelDescriptorConfigurationIdentifier extends Identifier
+public class ChannelDescriptorConfigurationIdentifier extends Identifier
{
/**
* Constructs a channel descriptor configuration identifier
*/
- public ChannelDescriptorConfigurationIdentifier(IChannelDescriptor value)
+ public ChannelDescriptorConfigurationIdentifier(IBandChannelDescriptor value)
{
super(value, IdentifierClass.CONFIGURATION, Form.CHANNEL_DESCRIPTOR, Role.ANY);
}
diff --git a/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java b/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java
index f14affd8..bd110c30 100644
--- a/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java
+++ b/src/main/java/io/github/dsheirer/identifier/encryption/EncryptionKeyIdentifier.java
@@ -30,7 +30,9 @@ import io.github.dsheirer.protocol.Protocol;
public class EncryptionKeyIdentifier extends Identifier
{
- public EncryptionKeyIdentifier(EncryptionKey value, IdentifierClass identifierClass, Form form, Role role)
+ private Protocol mProtocol;
+
+ public EncryptionKeyIdentifier(Protocol protocol, EncryptionKey value, IdentifierClass identifierClass, Form form, Role role)
{
super(value, identifierClass, form, role);
}
@@ -38,7 +40,7 @@ public class EncryptionKeyIdentifier extends Identifier
@Override
public Protocol getProtocol()
{
- return Protocol.APCO25;
+ return mProtocol;
}
public boolean isEncrypted()
@@ -46,8 +48,8 @@ public class EncryptionKeyIdentifier extends Identifier
return getValue() != null && getValue().isEncrypted();
}
- public static EncryptionKeyIdentifier create(EncryptionKey encryptionKey)
+ public static EncryptionKeyIdentifier create(Protocol protocol, EncryptionKey encryptionKey)
{
- return new EncryptionKeyIdentifier(encryptionKey, IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY);
+ return new EncryptionKeyIdentifier(protocol, encryptionKey, IdentifierClass.USER, Form.ENCRYPTION_KEY, Role.ANY);
}
}
diff --git a/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java b/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java
deleted file mode 100644
index 8c768bf8..00000000
--- a/src/main/java/io/github/dsheirer/module/carrier/CarrierOffsetProcessor.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- * ****************************************************************************
- */
-
-package io.github.dsheirer.module.carrier;
-
-import io.github.dsheirer.buffer.FloatAveragingBuffer;
-import io.github.dsheirer.dsp.window.WindowFactory;
-import io.github.dsheirer.dsp.window.WindowType;
-import io.github.dsheirer.sample.complex.ComplexSamples;
-import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
-import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
-import org.jtransforms.fft.FloatFFT_1D;
-
-/**
- * Calculates the carrier offset from channel center. Processes incoming Complex Sample buffers to detect the carrier
- * signal and calculate a carrier offset measurement.
- *
- * Calculates once a second by performing a 128-point FFT on each consecutive complex buffer. Once it detects a
- * sequence of 5 consecutive buffers with sufficient SNR (>15 dB), it calculates the average offset across those 5
- * buffers and then averages across the 10 most recent high-SNR sequences.
- */
-public class CarrierOffsetProcessor
-{
- private static final int FFT_BIN_SIZE = 128;
- private static final int MEASUREMENT_COUNT_THRESHOLD = 5;
- private static final int SEARCH_WINDOW_MIDDLE = FFT_BIN_SIZE / 2;
- private static final long CALCULATION_TIME_INTERVAL_MS = 1000;
- private static final float CENTER_INDEX = FFT_BIN_SIZE / 2.0f - 1;
- private static final float[] WINDOW = WindowFactory.getWindow(WindowType.BLACKMAN_HARRIS_7, FFT_BIN_SIZE * 2);
- private static final FloatFFT_1D FFT = new FloatFFT_1D(FFT_BIN_SIZE);
- private final FloatAveragingBuffer mAveragingBuffer = new FloatAveragingBuffer(5);
- private final FloatAveragingBuffer mOffsetAverage = new FloatAveragingBuffer(10, 3);
- private final StandardDeviation mStandardDeviation = new StandardDeviation();
- private float mCarrierOffset;
- private float mResolution;
- private int mHalfWidth;
- private int mMeasurementCount = 0;
- private int mSearchWindowMin;
- private int mSearchWindowMax;
- private long mEstimatedOffset;
- private long mLastCalculationTimestamp = 0;
-
- /**
- * Constructs an instance.
- */
- public CarrierOffsetProcessor()
- {
- setSampleRate(50000.0);
- }
-
- /**
- * Update the current channel sample rate.
- * @param sampleRate in Hertz
- */
- public void setSampleRate(double sampleRate)
- {
- mResolution = (float)sampleRate / FFT_BIN_SIZE;
- mHalfWidth = (int)Math.floor(12500.0 / mResolution / 2.0); //Half of 12.5 kHz signal width
- mSearchWindowMin = SEARCH_WINDOW_MIDDLE - mHalfWidth;
- mSearchWindowMax = SEARCH_WINDOW_MIDDLE + mHalfWidth;
- }
-
- /**
- * Resets this processor whenever the source tuner PPM value is changed or updated, so that we can immediately
- * recalculate the carrier offset with the updated settings.
- */
- public void reset()
- {
- mMeasurementCount = 0;
- mLastCalculationTimestamp = 0;
- mEstimatedOffset = 0;
- mAveragingBuffer.reset();
- mStandardDeviation.clear();
- mOffsetAverage.reset();
- }
-
- /**
- * Estimated carrier frequency offset averaged across buffers that have sufficient SNR (>15 dB).
- * @return estimated average carrier offset. Note: this method can be accessed after each invocation of process()
- * method.
- */
- public long getEstimatedOffset()
- {
- return mEstimatedOffset;
- }
-
- /**
- * Indicates if a signal was detected in the most recently processed sample buffer. A value of false indicates the
- * signal was either not present, or the SNR was less than 15 dB and therefore this processor could not calculate
- * the carrier offest estimation.
- * @return true if the most recent buffer had a signal that was also strong enough to process.
- */
- public boolean hasCarrier()
- {
- return mCarrierOffset != Float.MAX_VALUE;
- }
-
- /**
- * Processes the complex sample buffer by calculating the detected carrier offset and mixing the buffer samples by
- * remove the calculated offset. The current detected carrier offset is available via the getEstimatedOffset()
- * method.
- *
- * @return true if the estimated carrier offset value was updated.
- */
- public boolean process(ComplexSamples samples)
- {
- if(mLastCalculationTimestamp + CALCULATION_TIME_INTERVAL_MS < samples.timestamp())
- {
- int bufferOffset = 0;
-
- while((bufferOffset + FFT_BIN_SIZE) <= samples.length())
- {
- mCarrierOffset = calculateCarrierOffset(samples, bufferOffset);
-
- //Float max value indicates no signal or low SNR - reset the counter so that we can keep trying.
- if(mCarrierOffset == Float.MAX_VALUE)
- {
- mMeasurementCount = 0;
- mStandardDeviation.clear();
- }
- else
- {
- mMeasurementCount++;
- mAveragingBuffer.add(mCarrierOffset);
- }
-
- if(mMeasurementCount >= MEASUREMENT_COUNT_THRESHOLD)
- {
- if(mStandardDeviation.getResult() < 5.0)
- {
- mOffsetAverage.add(mAveragingBuffer.getAverage());
- mLastCalculationTimestamp = samples.timestamp();
-
- //Set the buffer offset to the sample length to exit the loop
- bufferOffset = samples.length();
- }
-
- mMeasurementCount = 0;
- mStandardDeviation.clear();
- }
-
- bufferOffset += FFT_BIN_SIZE;
- }
-
- mEstimatedOffset = (long)mOffsetAverage.getAverage();
- return true;
- }
-
- return false;
- }
-
- /**
- * Calculate the detected peak signal offset from the sample buffer.
- * @param complexSamples to process
- * @param offset into the complex samples buffer to calculate
- * @return estimated carrier offset from center of the channel.
- */
- private float calculateCarrierOffset(ComplexSamples complexSamples, int offset)
- {
- float[] samples = complexSamples.toInterleaved(offset, FFT_BIN_SIZE).samples();
- WindowFactory.apply(WINDOW, samples);
- FFT.complexForward(samples);
- float[] magnitudes = ComplexDecibelConverter.convert(samples);
- float peakIndex = findPeak(magnitudes);
-
- //Return float max value for Low SNR or no signal
- if(peakIndex == Float.MAX_VALUE)
- {
- return Float.MAX_VALUE;
- }
-
- mStandardDeviation.increment(peakIndex);
- return mResolution * (peakIndex - CENTER_INDEX);
- }
-
- /**
- * Finds the FFT bin with the largest magnitude within a window of +/- 12.5 kHz of the center bin.
- * @param magnitudes from the FFT measurement.
- * @return center bin or Float.MAX_VALUE to indicate no signal or low SNR.
- */
- private float findPeak(float[] magnitudes)
- {
- float peakValue = -150.0f;
- int peakIndex = 0;
- int min = mSearchWindowMin;
- int max = mSearchWindowMax;
- int half = mHalfWidth;
-
- //Find the peak magnitude within twice the search window width of the center indices.
- for(int x = min; x <= max; x++)
- {
- if(magnitudes[x] > peakValue)
- {
- peakValue = magnitudes[x];
- peakIndex = x;
- }
- }
-
- //From there, find the center point that best fits the signal to the search window width. We need a minimum
- // SNR for this to work.
- int left = peakIndex - half;
- int right = peakIndex + half;
-
- //Do we have enough SNR?
- float snr = peakValue - ((magnitudes[left] + magnitudes[right]) / 2.0f);
-
- if(snr > 15.0f)
- {
- float minimumSearchThreshold = peakValue - 16.0f; //Stop at bin values 16 db lower.
-
- while(magnitudes[left] < minimumSearchThreshold && left < peakIndex)
- {
- left++;
- }
-
- while(magnitudes[right] < minimumSearchThreshold && right > peakIndex)
- {
- right--;
- }
-
- int binWidth = right - left;
-
- if(binWidth == 0)
- {
- return peakIndex;
- }
- else
- {
- return left + (binWidth / 2.0f);
- }
- }
-
- //No signal or low SNR - return MAX VALUE to indicate no signal.
- return Float.MAX_VALUE;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index 64aa2b9c..8f7333dc 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -81,6 +81,12 @@ import io.github.dsheirer.module.decode.mpt1327.Sync;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState;
+import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
+import io.github.dsheirer.module.decode.nxdn.NXDNDecoder;
+import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState;
+import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager;
+import io.github.dsheirer.module.decode.nxdn.audio.NXDNAudioModule;
+import io.github.dsheirer.module.decode.nxdn.layer3.filter.NXDNMessageFilterSet;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
@@ -171,9 +177,6 @@ public class DecoderFactory
processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
trafficChannelManager, channelDescriptor);
break;
- case NBFM:
- processNBFM(channel, modules, aliasList, decodeConfig);
- break;
case LTR:
processLTRStandard(channel, modules, aliasList, (DecodeConfigLTRStandard) decodeConfig);
break;
@@ -184,6 +187,12 @@ public class DecoderFactory
processMPT1327(channelMapModel, channel, modules, aliasList, channelType,
(DecodeConfigMPT1327) decodeConfig, userPreferences);
break;
+ case NBFM:
+ processNBFM(channel, modules, aliasList, decodeConfig);
+ break;
+ case NXDN:
+ processNXDN(channel, userPreferences, modules, aliasList, decodeConfig, trafficChannelManager, channelDescriptor);
+ break;
case PASSPORT:
processPassport(channel, modules, aliasList, decodeConfig);
break;
@@ -462,6 +471,55 @@ public class DecoderFactory
}
}
+ /**
+ * Creates decoder modules for the NXDN decoder.
+ * @param channel configuration with center frequency
+ * @param modules to receive the decoder modules
+ * @param aliasList for aliases.
+ * @param decodeConfig with details
+ */
+ private static void processNXDN(Channel channel, UserPreferences userPreferences, List modules,
+ AliasList aliasList, DecodeConfiguration decodeConfig,
+ TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor)
+ {
+ if(decodeConfig instanceof DecodeConfigNXDN configNXDN)
+ {
+ //Add a channel rotation monitor when we have multiple control channel frequencies specified
+ if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf &&
+ sctmf.hasMultipleFrequencies())
+ {
+ List activeStates = new ArrayList<>();
+ activeStates.add(State.CONTROL);
+ modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences));
+ }
+
+ modules.add(new NXDNDecoder(configNXDN));
+ modules.add(new NXDNAudioModule(userPreferences, aliasList));
+
+ if(channel.getChannelType() == ChannelType.STANDARD)
+ {
+ NXDNTrafficChannelManager primaryTCM = new NXDNTrafficChannelManager(channel);
+ modules.add(primaryTCM);
+ modules.add(new NXDNDecoderState(channel, primaryTCM));
+ }
+ else if(trafficChannelManager instanceof NXDNTrafficChannelManager parentTCM)
+ {
+ NXDNDecoderState decoderState = new NXDNDecoderState(channel, parentTCM);
+ decoderState.setCurrentChannel(channelDescriptor);
+ modules.add(decoderState);
+ }
+ else
+ {
+ mLog.warn("Expected non-null NXDN traffic channel manager for channel " + channel.getName());
+ }
+
+ }
+ else
+ {
+ throw new IllegalArgumentException("Can't create NXDN decoder - unrecognized config: " + decodeConfig);
+ }
+ }
+
/**
* Creates modules for DMR decoder setup.
*
@@ -622,9 +680,9 @@ public class DecoderFactory
for(Module module : modules)
{
- if(module instanceof Decoder)
+ if(module instanceof Decoder decoder)
{
- filterSet.addFilters(getMessageFilter(((Decoder)module).getDecoderType()));
+ filterSet.addFilters(getMessageFilter(decoder.getDecoderType()));
}
}
@@ -668,6 +726,9 @@ public class DecoderFactory
case MPT1327:
filters.add(new MPT1327MessageFilter());
break;
+ case NXDN:
+ filters.add(new NXDNMessageFilterSet());
+ break;
case P25_PHASE1:
filters.add(new P25P1MessageFilterSet());
break;
@@ -708,6 +769,8 @@ public class DecoderFactory
return new DecodeConfigMPT1327();
case NBFM:
return new DecodeConfigNBFM();
+ case NXDN:
+ return new DecodeConfigNXDN();
case PASSPORT:
return new DecodeConfigPassport();
case P25_PHASE1:
@@ -767,6 +830,13 @@ public class DecoderFactory
copyNBFM.setSquelchNoiseCloseThreshold(origNBFM.getSquelchNoiseCloseThreshold());
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
return copyNBFM;
+ case NXDN:
+ DecodeConfigNXDN origNXDN = (DecodeConfigNXDN)config;
+ DecodeConfigNXDN copyNXDN = new DecodeConfigNXDN();
+ copyNXDN.setChannelMap(origNXDN.getChannelMap());
+ copyNXDN.setTransmissionMode(origNXDN.getTransmissionMode());
+ copyNXDN.setTrafficChannelPoolSize(origNXDN.getTrafficChannelPoolSize());
+ return copyNXDN;
case P25_PHASE1:
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;
DecodeConfigP25Phase1 copyP25 = new DecodeConfigP25Phase1();
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
index 02311d9c..5dcfafed 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,11 +28,12 @@ public enum DecoderType
{
//Primary Decoders
AM("AM", "AM", Protocol.UNKNOWN),
- DMR("DMR","DMR", Protocol.DMR),
+ DMR("DMR", "DMR", Protocol.DMR),
LTR("LTR", "LTR", Protocol.LTR),
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
NBFM("NBFM", "NBFM", Protocol.NBFM),
+ NXDN("NXDN", "NXDN", Protocol.NXDN),
PASSPORT("Passport", "Passport", Protocol.PASSPORT),
P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25),
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
@@ -44,9 +45,9 @@ public enum DecoderType
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
TAIT_1200("Tait 1200", "Tait 1200", Protocol.TAIT1200);
- private String mDisplayString;
- private String mShortDisplayString;
- private Protocol mProtocol;
+ private final String mDisplayString;
+ private final String mShortDisplayString;
+ private final Protocol mProtocol;
DecoderType(String displayString, String shortDisplayString, Protocol protocol)
{
@@ -58,38 +59,23 @@ public enum DecoderType
/**
* Primary decoders that operate on I/Q sample streams
*/
- public static EnumSet PRIMARY_DECODERS =
- EnumSet.of(DecoderType.AM,
- DecoderType.DMR,
- DecoderType.LTR,
- DecoderType.LTR_NET,
- DecoderType.MPT1327,
- DecoderType.NBFM,
- DecoderType.P25_PHASE1,
- DecoderType.P25_PHASE2,
- DecoderType.PASSPORT);
+ public static final EnumSet PRIMARY_DECODERS = EnumSet.of(AM, DMR, LTR, LTR_NET, MPT1327, NBFM, NXDN,
+ P25_PHASE1, P25_PHASE2, PASSPORT);
/**
* Auxiliary decoders that operate on in-band signalling in the decoded audio channel
*/
- public static final EnumSet AUX_DECODERS =
- EnumSet.of(DecoderType.DCS,
- DecoderType.FLEETSYNC2,
- DecoderType.LJ_1200,
- DecoderType.MDC1200,
- DecoderType.TAIT_1200);
+ public static final EnumSet AUX_DECODERS = EnumSet.of(DCS, FLEETSYNC2, LJ_1200, MDC1200, TAIT_1200);
/**
* Decoders that produce a (recordable) bitstream
*/
- public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR,
- DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
+ public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DMR, MPT1327, NXDN, P25_PHASE1, P25_PHASE2);
/**
* Decoders that produce (recordable) MBE audio codec frames
*/
- public static final EnumSet MBE_AUDIO_CODEC_DECODERS =
- EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
+ public static final EnumSet MBE_AUDIO_CODEC_DECODERS = EnumSet.of(DMR, NXDN, P25_PHASE1, P25_PHASE2);
public Protocol getProtocol()
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java b/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java
index 7aa5c283..c3cdf8d7 100644
--- a/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/FeedbackDecoder.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,8 +29,19 @@ import io.github.dsheirer.source.SourceEvent;
*/
public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceEventProvider
{
+ private static final double TWO_PI = 2 * Math.PI;
private Listener mSourceEventListener;
private Listener mSymbolListener;
+ private double mSampleRate;
+
+ /**
+ * Sets the decimated final sample rate that will be used for reporting decoder PLL errors.
+ * @param rate in Hertz
+ */
+ protected void setDecimatedSampleRate(double rate)
+ {
+ mSampleRate = rate;
+ }
/**
* Protocol description suitable for display in the user interface
@@ -68,13 +79,19 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE
}
/**
- * Processes the current phase-locked loop (PLL) measurement and sends to the parent tuner for self correction.
- * @param pllError as measured in the decoder.
+ * Processes the current phase-locked loop (PLL) measurement and sends to the channel frequency error manager to
+ * correct the error by mixing the incoming sample stream to remove the error.
+ *
+ * Note: this source event error measurement is rebroadcast by the ProcessingChain and received by the
+ * ChannelFrequencyErrorManager that lives within the TunerChannelSource where it adjusts the channel's mixer to
+ * incrementally correct and drive the error toward zero in concert with the parent tuner's auto PPM correction.
+ *
+ * @param pllError as measured in the decoder, in Radians.
*/
- public void processPLLError(float pllError, int baudRate)
+ public void processPLLError(float pllError)
{
- long frequencyError = (long)(baudRate / 2.0 * (pllError / Math.PI));
- broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(frequencyError, "Decoder measured error sync locked"));
+ long correctionRequestHertz = (long)(mSampleRate / TWO_PI * pllError);
+ broadcast(SourceEvent.frequencyCorrectionRequest(correctionRequestHertz));
}
/**
@@ -105,4 +122,14 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE
mSymbolListener.receive(symbol);
}
}
+
+ public String getEqualizerSettings()
+ {
+ return "Not implemented";
+ }
+
+ public void resetEqualizer()
+ {
+ //No-op, unless implemented in subclass.
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
index b1d579c8..f7f1d180 100644
--- a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
+++ b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.analog;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
@@ -42,7 +42,7 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource
private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class);
private Listener mSourceEventListener = new SourceEventListener();
private DecodeEvent mDecodeEvent;
- private IChannelDescriptor mChannelDescriptor = null;
+ private IBandChannelDescriptor mChannelDescriptor = null;
public AnalogDecoderState()
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
index 9b45da7f..e25f26e9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
+++ b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,6 +31,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
+import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25;
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@@ -39,13 +40,14 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DecodeConfigAM.class, name = "decodeConfigAM"),
+ @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR"),
@JsonSubTypes.Type(value = DecodeConfigLTRNet.class, name = "decodeConfigLTRNet"),
@JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"),
@JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"),
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
+ @JsonSubTypes.Type(value = DecodeConfigNXDN.class, name = "decodeConfigNXDN"),
@JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"),
- @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"),
- @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
+ @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport")
})
@JacksonXmlRootElement(localName = "decode_configuration")
public abstract class DecodeConfiguration extends Configuration
@@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration
{
return new int[0];
}
+
+ @JsonIgnore
+ public int getBitRate()
+ {
+ return getDecoderType().getProtocol().getBitRate();
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
index 51982a91..49502e4b 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,10 +28,8 @@ import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
-import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
-import io.github.dsheirer.module.carrier.CarrierOffsetProcessor;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
@@ -48,7 +46,6 @@ import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
-import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
@@ -82,8 +79,8 @@ import org.slf4j.LoggerFactory;
* The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and
* embedded link control parameters.
*/
-public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
- ISourceEventProvider, Listener
+public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
+ ISourceEventListener, Listener
{
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
@@ -100,8 +97,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
private IRealDecimationFilter mDecimationFilterQ;
private RealFIRFilter mRRCFilterI;
private RealFIRFilter mRRCFilterQ;
- private final PowerMonitor mPowerMonitor = new PowerMonitor();
- private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor();
/**
* Constructs an instance
@@ -140,9 +135,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
SYMBOL_RATE + " symbol rate)");
}
- mPowerMonitor.setSampleRate((int)sampleRate);
- mCarrierOffsetProcessor.setSampleRate(sampleRate);
-
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
@@ -158,6 +150,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
float decimatedSampleRate = (float)sampleRate / decimation;
+
+ //Set the decimated sample rate to use for PLL error reporting.
+ setDecimatedSampleRate(decimatedSampleRate);
+
float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate));
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
symbolLength += symbolLength % 2; //Make the symbol length even
@@ -190,9 +186,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
- //Process buffer for power measurements
- mPowerMonitor.process(i, q);
-
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
@@ -201,25 +194,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
float[] demodulated = mDemodulator.demodulate(i, q);
mSymbolProcessor.receive(demodulated);
-
- //Estimate carrier offset and broadcast at each update. This value is used in the channel spectral display,
- // and it's also processed by the tuner's PPM error monitor to auto-adjust the tuner PPM value.
- if(mCarrierOffsetProcessor.process(samples))
- {
- //Tuner PPM Monitor - negate the value to indicate channel error from tuner's PPM that's causing the offset
- mPowerMonitor.broadcast(SourceEvent.frequencyErrorMeasurement(-mCarrierOffsetProcessor.getEstimatedOffset()));
-
- //Channel spectral display - when there's a carrier send the estimate, otherwise send a zero to cause the
- //display to blank the carrier offset measurement indicator line
- if(mCarrierOffsetProcessor.hasCarrier())
- {
- mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(mCarrierOffsetProcessor.getEstimatedOffset()));
- }
- else
- {
- mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(0));
- }
- }
}
/**
@@ -299,21 +273,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
return "DMR C4FM";
}
- /**
- * Sets the source event listener to receive source events from this decoder.
- */
- @Override
- public void setSourceEventListener(Listener listener)
- {
- mPowerMonitor.setSourceEventListener(listener);
- }
-
- @Override
- public void removeSourceEventListener()
- {
- mPowerMonitor.setSourceEventListener(null);
- }
-
@Override
public void start()
{
@@ -340,9 +299,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
- case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
- mCarrierOffsetProcessor.reset();
- break;
}
}
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java
index 93913897..19251a86 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSoftSymbolProcessor.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2025 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -67,7 +67,6 @@ public class DMRSoftSymbolProcessor
private static final int BUFFER_PROTECTED_REGION_DIBITS = 92;
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS;
- private static final int SYMBOL_RATE = 4800;
private static final float EQUALIZER_LOOP_GAIN = 0.15f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
@@ -565,7 +564,7 @@ public class DMRSoftSymbolProcessor
mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE);
- mFeedbackDecoder.processPLLError(mEqualizerBalance, SYMBOL_RATE);
+ mFeedbackDecoder.processPLLError(mEqualizerBalance);
//Constrain gain between 1.0f and 1.35f
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java
index f463cf02..67441e5d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@
package io.github.dsheirer.module.decode.dmr;
import com.google.common.eventbus.Subscribe;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelConversionRequest;
@@ -225,7 +225,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
* @param currentFrequency of the standard channel
* @param restChannel to identify the new channel frequency to start
*/
- public void convertToTrafficChannel(Channel channel, long currentFrequency, IChannelDescriptor restChannel,
+ public void convertToTrafficChannel(Channel channel, long currentFrequency, IBandChannelDescriptor restChannel,
DMRNetworkConfigurationMonitor networkConfigurationMonitor)
{
mLock.lock();
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java
index 970d6c94..65e586d9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -162,7 +162,7 @@ public class DecodeConfigDMR extends DecodeConfiguration
@Override
public ChannelSpecification getChannelSpecification()
{
- return new ChannelSpecification(50000.0, 12500, 6500.0, 7200.0);
+ return new ChannelSpecification(24000.0, 12500, 6500.0, 7200.0);
}
/**
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java
index da4917d3..41e874f5 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/channel/DMRChannel.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@
package io.github.dsheirer.module.decode.dmr.channel;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
@@ -35,7 +35,7 @@ import org.apache.commons.lang3.Validate;
*
* Note: timeslots are tracked as 1 and 2
*/
-public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
+public abstract class DMRChannel extends IntegerIdentifier implements IBandChannelDescriptor, ITimeslotFrequencyReceiver
{
private TimeslotFrequency mTimeslotFrequency;
private int mTimeslot;
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java
similarity index 84%
rename from src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java
rename to src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java
index 2999d549..03dc35b0 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelEventTracker.java
+++ b/src/main/java/io/github/dsheirer/module/decode/event/ChannelEventTracker.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
-package io.github.dsheirer.module.decode.p25;
+package io.github.dsheirer.module.decode.event;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Form;
@@ -28,33 +28,33 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic
- * channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need
- * for a new event.
+ * Base channel event tracker
+ * @param type of decode event being tracked.
*/
-public class P25TrafficChannelEventTracker
+public abstract class ChannelEventTracker
{
- private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class);
- private static final long STALE_EVENT_THRESHOLD_MS = 2000;
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChannelEventTracker.class);
+ private static final long STALE_EVENT_THRESHOLD_MS = 3000;
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
- private P25ChannelGrantEvent mEvent;
private boolean mStarted = false;
private boolean mComplete = false;
+ private final T mEvent;
/**
* Constructs an instance
- * @param event to track for the traffic channel.
+ * @param event to be tracked
*/
- public P25TrafficChannelEventTracker(P25ChannelGrantEvent event)
+ public ChannelEventTracker(T event)
{
mEvent = event;
}
/**
* Access the underlying traffic channel event that is being tracked.
+ *
* @return event.
*/
- public P25ChannelGrantEvent getEvent()
+ public T getEvent()
{
return mEvent;
}
@@ -62,6 +62,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if this event is stale relative to the provided timestamp. Staleness is determined by the time delta
* between the event's end time and timestamp argument.
+ *
* @param timestamp to check for staleness
* @return true if the delta time exceeds a threshold.
*/
@@ -76,23 +77,23 @@ public class P25TrafficChannelEventTracker
}
/**
- * Indicates if the TDMA data channel duration exceeds the threshold (15 seconds)
+ * Indicates if the data channel duration exceeds the threshold (15 seconds)
*/
- public boolean exceedsMaxTDMADataDuration()
+ public boolean exceedsMaxDataDuration()
{
return getEvent().getDuration() > MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS;
}
/**
* Adds the identifier to the tracked event if the event's identifier collection does not already have it.
+ *
* @param identifier to add
*/
public void addIdentifierIfMissing(Identifier identifier)
{
if(identifier != null && !getEvent().getIdentifierCollection().hasIdentifier(identifier))
{
- MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection()
- .getIdentifiers());
+ MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection().getIdentifiers());
mic.update(identifier);
getEvent().setIdentifierCollection(mic);
}
@@ -101,7 +102,8 @@ public class P25TrafficChannelEventTracker
/**
* Adds the additional details to the tracked event if the current event details are null or if they do not contain
* the additional details.
- * @param additionalDetails
+ *
+ * @param additionalDetails for the event
*/
public void addDetailsIfMissing(String additionalDetails)
{
@@ -135,6 +137,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the tracked event from identifier is non null and that it is different to the from argument.
+ *
* @param fromToCompare against the current event from identifier.
* @return true if they are different.
*/
@@ -153,6 +156,7 @@ public class P25TrafficChannelEventTracker
/**
* Checks the call for staleness and verifies that the call event TO identifier is equal to the TO identifier
* in the provided identifier collection.
+ *
* @param toCompareIC containing a TO and optionally a FROM identifier to compare.
* @param timestamp to check for staleness
* @return true if the call identifiers are the same and the call is not stale.
@@ -195,6 +199,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the event has been marked as complete by traffic channel signalling.
+ *
* @return complete status.
*/
public boolean isComplete()
@@ -204,6 +209,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the event has been marked as started by the traffic channel for HDU or LDU signalling.
+ *
* @return started status.
*/
public boolean isStarted()
@@ -236,6 +242,7 @@ public class P25TrafficChannelEventTracker
*
* Note: once the event is being updated from the traffic channel, any attempts to update from the control channel
* are ignored.
+ *
* @param timestamp to assign.
*/
public void updateDurationTraffic(long timestamp)
@@ -255,6 +262,7 @@ public class P25TrafficChannelEventTracker
* Mark the event as complete and assign final end timestamp to the event.
*
* Note: further attempts to complete an already complete event are ignored.
+ *
* @param timestamp to assign.
* @return true if the timestamp was updated
*/
@@ -272,6 +280,7 @@ public class P25TrafficChannelEventTracker
/**
* Updates the details for the tracked event.
+ *
* @param details to update
*/
public void setDetails(String details)
@@ -279,8 +288,46 @@ public class P25TrafficChannelEventTracker
getEvent().setDetails(details);
}
+ /**
+ * Prefixes the details the current event details text
+ * @param details to prefix
+ */
+ public void prefixDetails(String details)
+ {
+ if(getEvent().getDetails() == null)
+ {
+ getEvent().setDetails(details);
+ }
+ else
+ {
+ if(!getEvent().getDetails().contains(details))
+ {
+ getEvent().setDetails(details + " " + getEvent().getDetails());
+ }
+ }
+ }
+
+ /**
+ * Adds the details to the end of the existing event text or sets the event details.
+ */
+ public void addDetails(String details)
+ {
+ if(getEvent().getDetails() == null)
+ {
+ getEvent().setDetails(details);
+ }
+ else
+ {
+ if(!getEvent().getDetails().contains(details))
+ {
+ getEvent().setDetails(getEvent().getDetails() + " " + details);
+ }
+ }
+ }
+
/**
* Updates the channel descriptor for the tracked event.
+ *
* @param channelDescriptor to update.
*/
public void addChannelDescriptorIfMissing(IChannelDescriptor channelDescriptor)
diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java
index 68f35a95..f088bdbd 100644
--- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java
+++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEvent.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java
index ff0497da..b6de52ed 100644
--- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java
+++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventDuplicateDetector.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -101,12 +101,19 @@ public class DecodeEventDuplicateDetector
}
/**
- * Generates a unique event key that is a combinatio of the TO identifier value and the details of the event.
+ * Generates a unique event key that is a combination of the TO identifier value and the details of the event.
* @param event for the key
* @return generated key.
*/
private static String getKey(IDecodeEvent event)
{
+ if(event.getIdentifierCollection().getFromIdentifier() != null)
+ {
+ return event.getIdentifierCollection().getToIdentifier().toString() +
+ event.getIdentifierCollection().getFromIdentifier().toString() +
+ event.getDetails();
+ }
+
return event.getIdentifierCollection().getToIdentifier().toString() + event.getDetails();
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java
index 82be10c4..0d54ce7d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventModel.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2024 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java
index 55609355..113ffea4 100644
--- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventPanel.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -59,24 +59,24 @@ import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
+/**
+ * Decode event table view
+ */
public class DecodeEventPanel extends JPanel implements Listener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(DecodeEventPanel.class);
private static final String TABLE_PREFERENCE_KEY = "decode.event.panel";
- private JTable mTable;
- private JTableColumnWidthMonitor mTableColumnWidthMonitor;
- private DecodeEventModel mEventModel = new DecodeEventModel();
+ private final JTable mTable;
+ private final DecodeEventModel mEventModel = new DecodeEventModel();
private DecodeEventHistory mCurrentEventHistory;
- private JScrollPane mEmptyScroller;
- private IconModel mIconModel;
- private AliasModel mAliasModel;
- private UserPreferences mUserPreferences;
- private TimestampCellRenderer mTimestampCellRenderer;
- private FilterSet mFilterSet = new DecodeEventFilterSet();
- private TableRowSorter mTableRowSorter;
- private HistoryManagementPanel mHistoryManagementPanel;
+ private final IconModel mIconModel;
+ private final AliasModel mAliasModel;
+ private final UserPreferences mUserPreferences;
+ private final TimestampCellRenderer mTimestampCellRenderer;
+ private final FilterSet mFilterSet = new DecodeEventFilterSet();
+ private final HistoryManagementPanel mHistoryManagementPanel;
/**
@@ -93,19 +93,19 @@ public class DecodeEventPanel extends JPanel implements Listener(mEventModel);
- mTableRowSorter.setRowFilter(new EventRowFilter());
- mTable.setRowSorter(mTableRowSorter);
- mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
+ TableRowSorter tableRowSorter = new TableRowSorter<>(mEventModel);
+ tableRowSorter.setRowFilter(new EventRowFilter());
+ mTable.setRowSorter(tableRowSorter);
+ JTableColumnWidthMonitor tableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
updateCellRenderers();
mHistoryManagementPanel = new HistoryManagementPanel<>(mEventModel, "Event Filter Editor");
mHistoryManagementPanel.updateFilterSet(mFilterSet);
add(mHistoryManagementPanel, "span,growx");
- mEmptyScroller = new JScrollPane(mTable);
- add(mEmptyScroller);
+ JScrollPane emptyScroller = new JScrollPane(mTable);
+ add(emptyScroller);
//Register filter change listener to refresh the table any time the event filters are changed.
- mFilterSet.register(() -> mEventModel.fireTableDataChanged());
+ mFilterSet.register(mEventModel::fireTableDataChanged);
}
public void dispose()
@@ -122,7 +122,7 @@ public class DecodeEventPanel extends JPanel implements Listener mTimestampCellRenderer.updatePreferences());
+ EventQueue.invokeLater(mTimestampCellRenderer::updatePreferences);
}
}
@@ -215,7 +215,7 @@ public class DecodeEventPanel extends JPanel implements Listener 0)
+ if(!sb.isEmpty())
{
sb.append(",");
}
@@ -262,9 +262,8 @@ public class DecodeEventPanel extends JPanel implements Listener identifiers = identifierCollection.getIdentifiers(mRole);
if(identifiers != null && !identifiers.isEmpty())
@@ -281,13 +280,13 @@ public class DecodeEventPanel extends JPanel implements Listener 0)
+ if(!sb.isEmpty())
{
sb.append(",");
}
sb.append(Joiner.on(", ").skipNulls().join(aliases));
- color = aliases.get(0).getDisplayColor();
- icon = mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE);
+ color = aliases.getFirst().getDisplayColor();
+ icon = mIconModel.getIcon(aliases.getFirst().getIconName(), IconModel.DEFAULT_ICON_SIZE);
}
}
@@ -337,9 +336,9 @@ public class DecodeEventPanel extends JPanel implements Listener 0)
@@ -409,7 +406,7 @@ public class DecodeEventPanel extends JPanel implements Listener 0)
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java
index b0fa9f52..e06a9bd8 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/channel/LtrNetChannel.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2019 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,16 +14,16 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.module.decode.ltrnet.channel;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
-public class LtrNetChannel implements IChannelDescriptor, Comparable
+public class LtrNetChannel implements IBandChannelDescriptor, Comparable
{
private long mDownlink;
private long mUplink;
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java
index 14a894bf..69b69b3f 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/channel/LtrChannel.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2019 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,16 +14,16 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.module.decode.ltrstandard.channel;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
-public class LtrChannel implements IChannelDescriptor, Comparable
+public class LtrChannel implements IBandChannelDescriptor, Comparable
{
private long mDownlink;
private int mChannel;
diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java
index 7e249d07..68712bc7 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327ChannelGrantEvent.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2023 Dennis Sheirer
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.mpt1327;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.protocol.Protocol;
@@ -52,7 +52,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
protected long mDuration;
protected DecodeEventType mDecodeEventType;
protected IdentifierCollection mIdentifierCollection;
- protected IChannelDescriptor mChannelDescriptor;
+ protected IBandChannelDescriptor mChannelDescriptor;
protected String mDetails;
/**
@@ -90,7 +90,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
* Sets the channel descriptor for this event
* @param channelDescriptor
*/
- public MPT1327ChannelGrantDecodeEventBuilder channel(IChannelDescriptor channelDescriptor)
+ public MPT1327ChannelGrantDecodeEventBuilder channel(IBandChannelDescriptor channelDescriptor)
{
mChannelDescriptor = channelDescriptor;
return this;
diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java
index f6c75562..18d61f76 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/channel/MPT1327Channel.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2019 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,22 +14,21 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.module.decode.mpt1327.channel;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
-
import java.util.Objects;
/**
* MPT-1327 Channel
*/
-public class MPT1327Channel implements IChannelDescriptor
+public class MPT1327Channel implements IBandChannelDescriptor
{
private int mChannelNumber;
private ChannelMap mChannelMap;
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java
new file mode 100644
index 00000000..7a907ef7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java
@@ -0,0 +1,218 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+package io.github.dsheirer.module.decode.nxdn;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * NXDN decoder configuration
+ */
+public class DecodeConfigNXDN extends DecodeConfiguration
+{
+ public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 200;
+ public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500;
+ public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000;
+ private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
+ private boolean mIgnoreDataCalls = false;
+ private boolean mIgnoreEncryptedCalls = false;
+
+ private static final ChannelSpecification CHANNEL_4800 = new ChannelSpecification(12000.0, 6250, 3000.0, 3125.0);
+ private static final ChannelSpecification CHANNEL_9600 = new ChannelSpecification(24000.0, 12500, 5200.0, 6250.0);
+ private TransmissionMode mTransmissionMode = TransmissionMode.M4800;
+ private List mChannelMap = new ArrayList<>();
+ private Encoding mEncoding = Encoding.UTF8;
+
+ /**
+ * Default constructor for Jackson.
+ */
+ public DecodeConfigNXDN()
+ {
+ }
+
+ /**
+ * Override the parent method to indicate 4800 or 9600 bits per second, depending on the mode.
+ */
+ @Override
+ public int getBitRate()
+ {
+ return getTransmissionMode().getSymbolRate() * 2;
+ }
+
+ /**
+ * Constructs an instance
+ * @param transmissionMode for the system
+ */
+ public DecodeConfigNXDN(TransmissionMode transmissionMode)
+ {
+ mTransmissionMode = transmissionMode;
+ }
+
+ /**
+ * Optional user-provided channel to frequency mapping
+ * @return map
+ */
+ @JacksonXmlProperty(localName = "channelMap")
+ public List getChannelMap()
+ {
+ return mChannelMap;
+ }
+
+ /**
+ * Sets the optional user-provided channel to frequency mapping
+ * @param channelMap with entries to configure channels
+ */
+ public void setChannelMap(List channelMap)
+ {
+ mChannelMap = channelMap;
+ }
+
+ /**
+ * Adds the channel frequency entry to the map
+ * @param channelFrequency to add
+ */
+ public void add(ChannelFrequency channelFrequency)
+ {
+ mChannelMap.add(channelFrequency);
+ }
+
+ /**
+ * Ignore data calls
+ * @return true if ignore
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls")
+ public boolean isIgnoreDataCalls()
+ {
+ return mIgnoreDataCalls;
+ }
+
+ /**
+ * Set ignore data calls
+ * @param ignore data calls
+ */
+ public void setIgnoreDataCalls(boolean ignore)
+ {
+ mIgnoreDataCalls = ignore;
+ }
+
+ /**
+ * Ignore encrypted calls
+ * @return true if ignore
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "ignore_encrypted_calls")
+ public boolean isIgnoreEncryptedCalls()
+ {
+ return mIgnoreEncryptedCalls;
+ }
+
+ /**
+ * Set ignore encrypted calls
+ * @param ignore encrypted calls
+ */
+ public void setIgnoreEncryptedCalls(boolean ignore)
+ {
+ mIgnoreEncryptedCalls = ignore;
+ }
+
+ /**
+ * Traffic channel pool size.
+ * @return
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "traffic_channel_pool_size")
+ public int getTrafficChannelPoolSize()
+ {
+ return mTrafficChannelPoolSize;
+ }
+
+ /**
+ * Sets the traffic channel pool size which is the maximum number of
+ * simultaneous traffic channels that can be allocated.
+ *
+ * This limits the maximum calls so that busy systems won't cause more
+ * traffic channels to be allocated than the decoder/software/host computer
+ * can support.
+ */
+ public void setTrafficChannelPoolSize(int size)
+ {
+ mTrafficChannelPoolSize = size;
+ }
+
+ /**
+ * Transmission mode for this system.
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "mode")
+ public TransmissionMode getTransmissionMode()
+ {
+ return mTransmissionMode;
+ }
+
+ /**
+ * Sets/changes the transmission mode
+ * @param transmissionMode to use
+ */
+ public void setTransmissionMode(TransmissionMode transmissionMode)
+ {
+ mTransmissionMode = transmissionMode;
+ }
+
+ /**
+ * Character set for encoding talker alias values
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "charset")
+ public Encoding getEncoding()
+ {
+ return mEncoding;
+ }
+
+ /**
+ * Sets the character set for talker alias encoding
+ * @param encoding to use
+ */
+ public void setEncoding(Encoding encoding)
+ {
+ mEncoding = encoding;
+ }
+
+ @JsonIgnore
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.NXDN;
+ }
+
+ @JsonIgnore
+ @Override
+ public ChannelSpecification getChannelSpecification()
+ {
+ return switch(getTransmissionMode())
+ {
+ case M4800, TYPE_D -> CHANNEL_4800;
+ case M9600 -> CHANNEL_9600;
+ };
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java
new file mode 100644
index 00000000..12ab79de
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelEventTracker.java
@@ -0,0 +1,60 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.module.decode.event.ChannelEventTracker;
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+
+/**
+ * Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic
+ * channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need
+ * for a new event.
+ */
+public class NXDNChannelEventTracker extends ChannelEventTracker
+{
+ private boolean mTrafficChannelAllocated = false;
+
+ /**
+ * Constructs an instance
+ *
+ * @param event to track for the traffic channel.
+ */
+ public NXDNChannelEventTracker(DecodeEvent event)
+ {
+ super(event);
+ }
+
+ /**
+ * Indicates if a traffic channel was allocated for this event
+ */
+ public boolean isTrafficChannelAllocated()
+ {
+ return mTrafficChannelAllocated;
+ }
+
+ /**
+ * Sets flag to indicate if traffic channel was allocated
+ * @param allocated as the flag
+ */
+ public void setTrafficChannelAllocated(boolean allocated)
+ {
+ mTrafficChannelAllocated = allocated;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java
new file mode 100644
index 00000000..340c5ca9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNChannelInfoPreloadData.java
@@ -0,0 +1,43 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.controller.channel.event.PreloadDataContent;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
+import java.util.List;
+
+/**
+ * Channel access info and optional channel frequencies to preload into a traffic channel.
+ */
+public class NXDNChannelInfoPreloadData extends PreloadDataContent
+{
+ /**
+ * Constructs an instance
+ *
+ * @param info from the channel to preload
+ * @param frequencies optional provided by the user in the config
+ */
+ public NXDNChannelInfoPreloadData(ChannelAccessInformation info, List frequencies)
+ {
+ super(new ChannelInformation(info, frequencies));
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java
new file mode 100644
index 00000000..a09b51c9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecodeEvent.java
@@ -0,0 +1,54 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.protocol.Protocol;
+
+/**
+ * NXDN Decode Event
+ */
+public class NXDNDecodeEvent extends DecodeEvent
+{
+ /**
+ * Constructs a decode event
+ * @param decodeEventType for the event
+ * @param start timestamp
+ */
+ public NXDNDecodeEvent(DecodeEventType decodeEventType, long start)
+ {
+ super(decodeEventType, start);
+ setProtocol(Protocol.NXDN);
+ }
+
+ /**
+ * Creates a new decode event builder with the specified start timestamp.
+ * @param timeStart for the event
+ * @return builder
+ */
+ public static DecodeEventBuilder builder(DecodeEventType decodeEventType, long timeStart)
+ {
+ DecodeEventBuilder decodeEventBuilder = new DecodeEventBuilder(decodeEventType, timeStart);
+ decodeEventBuilder.protocol(Protocol.NXDN);
+ decodeEventBuilder.timeslot(0);
+ return decodeEventBuilder;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java
new file mode 100644
index 00000000..edff4637
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java
@@ -0,0 +1,413 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import com.google.common.eventbus.Subscribe;
+import io.github.dsheirer.dsp.filter.FilterFactory;
+import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
+import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
+import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
+import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
+import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
+import io.github.dsheirer.dsp.fm.FmDemodulatorFactory;
+import io.github.dsheirer.dsp.fm.IDemodulator;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.FeedbackDecoder;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.IByteBufferProvider;
+import io.github.dsheirer.sample.complex.ComplexSamples;
+import io.github.dsheirer.sample.complex.IComplexSamplesListener;
+import io.github.dsheirer.source.ISourceEventListener;
+import io.github.dsheirer.source.SourceEvent;
+import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
+import io.github.dsheirer.source.wave.ComplexWaveSource;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * NXDN Decoder for 4800, 9600 and Type-C/D systems.
+ */
+public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
+ ISourceEventListener, Listener
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class);
+ private static final Map BASEBAND_FILTERS = new HashMap<>();
+ private final DecodeConfigNXDN mConfig;
+ private final NXDNSymbolProcessor mSymbolProcessor;
+ private final NXDNMessageFramer mMessageFramer;
+ private final NXDNMessageProcessor mMessageProcessor;
+ private IRealDecimationFilter mDecimationFilterI;
+ private IRealDecimationFilter mDecimationFilterQ;
+ private IRealFilter mBasebandFilterI;
+ private IRealFilter mBasebandFilterQ;
+ private IRealFilter mPulseShapingFilter;
+ private IDemodulator mDemodulator;
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.NXDN;
+ }
+
+ /**
+ * Constructs an instance
+ * @param config for the decoder
+ */
+ public NXDNDecoder(DecodeConfigNXDN config)
+ {
+ mConfig = config;
+ mMessageProcessor = new NXDNMessageProcessor(config);
+ mMessageProcessor.setMessageListener(getMessageListener());
+ mMessageFramer = new NXDNMessageFramer(mMessageProcessor, config.getTransmissionMode());
+ mSymbolProcessor = new NXDNSymbolProcessor(mMessageFramer, this);
+ }
+
+ @Override
+ public void resetEqualizer()
+ {
+ mSymbolProcessor.resetEqualizer();
+ }
+
+ @Override
+ public String getEqualizerSettings()
+ {
+ return mSymbolProcessor.getEqualizerSettings();
+ }
+
+ /**
+ * Preloads NXDN channel details passed from the control channel used to resolve channel frequency information
+ * in traffic channel broadcast messaging.
+ *
+ * @param preloadData containing a DMR network configuration monitor.
+ */
+ @Subscribe
+ public void preload(NXDNChannelInfoPreloadData preloadData)
+ {
+ if(mMessageProcessor != null)
+ {
+ mMessageProcessor.preload(preloadData);
+ }
+ }
+
+ @Override
+ public String getProtocolDescription()
+ {
+ return "NXDN " + mConfig.getTransmissionMode();
+ }
+
+ /**
+ * Sets the sample rate and configures internal decoder components.
+ * @param sampleRate of the channel to decode
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ float samplesPerSymbol = (float)sampleRate / mConfig.getTransmissionMode().getSymbolRate();
+
+ if(samplesPerSymbol <= 4.0f)
+ {
+ throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be at least " + (mConfig.getTransmissionMode().getSymbolRate() * 4) + " Hz");
+ }
+
+ int decimation = 1;
+
+ //Identify decimation that gets us as close to 5.0 Samples Per Symbol as possible
+ while(samplesPerSymbol >= 10.0)
+ {
+ decimation *= 2;
+ samplesPerSymbol /= 2;
+ }
+
+ mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
+ mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
+ float decimatedSampleRate = (float)sampleRate / decimation;
+
+ //Set the decimated sample rate to use for PLL error reporting.
+ setDecimatedSampleRate(decimatedSampleRate);
+
+ int symbolLength = 26;
+ float rrcAlpha = 0.2f;
+ float[] rrcTaps = FilterFactory.getRootRaisedCosine(samplesPerSymbol, symbolLength, rrcAlpha);
+ mPulseShapingFilter = new RealFIRFilter(rrcTaps);
+ float[] basebandTaps = getBasebandFilter(decimatedSampleRate, mConfig);
+ mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps);
+ mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps);
+ mDemodulator = FmDemodulatorFactory.getFmDemodulator();
+ mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol);
+ mMessageProcessor.setMessageListener(getMessageListener());
+ }
+
+ /**
+ * Primary method for processing incoming complex sample buffers
+ * @param samples containing channelized complex samples
+ */
+ @Override
+ public void receive(ComplexSamples samples)
+ {
+ //Update the message framer with the timestamp from the incoming sample buffer.
+ mMessageFramer.setTimestamp(samples.timestamp());
+
+ float[] i = mDecimationFilterI.decimateReal(samples.i());
+ float[] q = mDecimationFilterQ.decimateReal(samples.q());
+
+ i = mBasebandFilterI.filter(i);
+ q = mBasebandFilterQ.filter(q);
+
+ float[] demodulated = mDemodulator.demodulate(i, q);
+ demodulated = mPulseShapingFilter.filter(demodulated);
+
+ //Process demodulated samples into symbols and apply message sync detection and framing.
+ mSymbolProcessor.process(demodulated);
+ }
+
+ /**
+ * Constructs a baseband filter for this decoder using the current sample rate
+ */
+ private float[] getBasebandFilter(double sampleRate, DecodeConfigNXDN config)
+ {
+
+ String mapKey = config.getTransmissionMode().name() + sampleRate;
+
+ float[] coefficients = null;
+
+ if(BASEBAND_FILTERS.containsKey(mapKey))
+ {
+ coefficients = BASEBAND_FILTERS.get(mapKey);
+ }
+ else
+ {
+ ChannelSpecification channel = config.getChannelSpecification();
+
+ FIRFilterSpecification specification = FIRFilterSpecification
+ .lowPassBuilder()
+ .sampleRate(sampleRate)
+ .passBandCutoff(channel.getPassFrequency())
+ .passBandAmplitude(1.0).passBandRipple(0.01)
+ .stopBandAmplitude(0.0).stopBandStart(channel.getStopFrequency())
+ .stopBandRipple(0.01).build();
+
+ try
+ {
+ coefficients = FilterFactory.getTaps(specification);
+
+ if(coefficients != null)
+ {
+ BASEBAND_FILTERS.put(mapKey, coefficients);
+ }
+ }
+ catch(Exception fde) //FilterDesignException
+ {
+ System.out.println("Error");
+ }
+ }
+
+ if(coefficients == null)
+ {
+ throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]");
+ }
+
+ return coefficients;
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the symbol processor
+ */
+ @Override
+ public void setBufferListener(Listener listener)
+ {
+ mSymbolProcessor.setBufferListener(listener);
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the symbol processor
+ */
+ @Override
+ public void removeBufferListener(Listener listener)
+ {
+ mSymbolProcessor.setBufferListener(null);
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the symbol processor
+ */
+ @Override
+ public boolean hasBufferListeners()
+ {
+ return mSymbolProcessor.hasBufferListener();
+ }
+
+ @Override
+ public Listener getSourceEventListener()
+ {
+ return this::process;
+ }
+
+ @Override
+ public void stop()
+ {
+ super.stop();
+ mSymbolProcessor.dispose();
+ }
+
+ /**
+ * Process source events
+ */
+ private void process(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ mSymbolProcessor.resetBalance();
+ break;
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ setSampleRate(sourceEvent.getValue().doubleValue());
+ break;
+ }
+ }
+
+ @Override
+ public Listener getComplexSamplesListener()
+ {
+ return this;
+ }
+
+ public static class MyListener implements Listener
+ {
+ private int mInvalidMessages;
+ private int mValidMessages;
+ private int mValidMessageBitErrors;
+
+ public void log()
+ {
+ System.out.println("Invalid: " + mInvalidMessages + " Valid: " + mValidMessages + " Valid-Bit Errors: " + mValidMessageBitErrors);
+ }
+
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message instanceof NXDNMessage nxdn)
+ {
+ if(nxdn.isValid())
+ {
+ System.out.println("BE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn);
+ mValidMessages++;
+ mValidMessageBitErrors += nxdn.getMessage().getCorrectedBitCount();
+ }
+ else
+ {
+ System.out.println("\t\tBE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn);
+ mInvalidMessages++;
+ }
+ }
+ else
+ {
+ System.out.println(message.toString());
+ }
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ LOGGER.info("Starting ...");
+
+ DecodeConfigNXDN config = null;
+// String directory = "D:\\Recordings\\NXDN\\"; //Windows
+ String directory = "/run/media/denny/T9/Recordings/NXDN/"; //Linux
+
+ //NXDN 9600 Channels
+// config = new DecodeConfigNXDN(TransmissionMode.M9600);
+// config.add(new ChannelFrequency(105, 451887500, 0));
+// String file = directory + "20251127_063701_451887500_Bush-NXDN-96_Sentinel-Heights_Control_45_baseband.wav";
+
+ //Weird 938 MHz site in Springfield, VA that is all CRC errors.
+// String file = directory + "20260324_033833_938736125_SYSTEM_SITE_null_47_baseband.wav";
+
+ //NXDN 4800 Channels
+ config = new DecodeConfigNXDN(TransmissionMode.M4800);
+ String file = directory + "20251128_052514_150845000_MobileTech-NXDN-48_Fulton_Control_47_baseband.wav";
+ //Lower SNR recordings
+// String file = directory + "20260430_023716_150867500_Mobiletech-Communications-(NXDN)_Pulaski_Pulaski_11_baseband.wav";
+// String file = directory + "20260430_023556_152982500_Mobiletech-Communications-(NXDN)_Manlius_Manlius_15_baseband.wav";
+
+ //This traffic channel sample Has Sync Detects between 1,096,430 - 1,111,430 samples (decimated sample rate: 12,500 Hz)
+// String file = directory + "20260104_065056_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav";
+// String file = directory + "20260104_065240_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav";
+// String file = directory + "AceVentura Sample 1/20260122_071724_154987500_SCFR_SITE_null_1_baseband.wav";
+// String file = directory + "AceVentura Sample 2/20260122_073239_154987500_SCFR_SITE_null_1_baseband.wav";
+// String file = directory + "20260322_063443_150845000_Mobiletech-Communications-(NXDN)_Fulton_Fulton_2_baseband.wav";
+
+
+ //Type-D Channels
+// config = new DecodeConfigNXDN(TransmissionMode.TYPE_D);
+// String file = directory + "Type C and D/Action Radio Hennepin tadscottsmith/20260402_152326_464125000_Action-Radio---Hennepin_LCN-1_103_baseband.wav";
+// String file = directory + "Type C and D/Action Radio Hennepin 2 tadscottsmith/recordings/20260330_221120_464650000_Action-Radio_Hennepin_LCN-4_92_baseband.wav";
+// String file = directory + "Type C and D/20260314_091952_451775000_Heimes_Omaha_TYPE_D_114_baseband.wav";
+// String file = directory + "Type C and D/20260318_034719_452675000_Heimes_Omaha_TYPE_D_0_baseband.wav";
+// String file = directory + "Type C and D/20260320_065339_451450000_Heimes_Omaha_TYPE_D_0_baseband.wav";
+
+ //This file has PPM mis-adjusted by 8.0 to test for equalizer balance
+// String file = directory + "20260119_044740_150845000_Mobiletech-Communications-(NXDN)_Oswego_Control_2_baseband.wav";
+
+ System.out.println("Processing File: " + file);
+ boolean autoReplay = false;
+
+ NXDNDecoder decoder = new NXDNDecoder(config);
+ decoder.start();
+
+ MyListener listener = new MyListener();
+ decoder.setMessageListener(listener);
+
+ try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay))
+ {
+ source.setListener(iNativeBuffer -> {
+ Iterator it = iNativeBuffer.iterator();
+
+ while(it.hasNext())
+ {
+ decoder.receive(it.next());
+ }
+ });
+ source.start();
+ decoder.setSampleRate(source.getSampleRate());
+ System.out.println("Recording File Sample Rate: " + source.getSampleRate());
+
+ while(true)
+ {
+ source.next(2048, true);
+ }
+ }
+ catch(IOException ioe)
+ {
+ LOGGER.error("Error", ioe);
+ }
+
+ listener.log();
+
+ LOGGER.info("Finished");
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java
new file mode 100644
index 00000000..df0acfe3
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoderState.java
@@ -0,0 +1,738 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
+import io.github.dsheirer.channel.state.DecoderState;
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.channel.state.State;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.identifier.Form;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.identifier.MutableIdentifierCollection;
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.identifier.radio.RadioIdentifier;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
+import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignmentDuplicateControl;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallReceptionRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequestWithESN;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallRequestHeader;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateControl;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallConnectionResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallReceptionRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS;
+import io.github.dsheirer.module.decode.nxdn.layer3.data.NXDNPacketMessage;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequest;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequestMultiSystem;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponseMultiSystem;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationParameterInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponseTypeD;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationClearResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationCommand;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponse;
+import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponseTypeD;
+import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasComplete;
+import io.github.dsheirer.protocol.Protocol;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * NXDN decoder state
+ */
+public class NXDNDecoderState extends DecoderState
+{
+ private static final int IDLE_DURING_CALL_MAX_COUNT = 5;
+ private final Channel mChannel;
+ private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor();
+ private final NXDNTrafficChannelManager mTrafficChannelManager;
+ private boolean mEncryptedCallStateDetermined = false;
+ private boolean mEncryptedCall = false;
+ private int mDisconnectResponseCount = 0;
+ private int mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
+
+ /**
+ * Constructs an instance
+ * @param channel for this state
+ * @param trafficChannelManager shared with this channel
+ */
+ public NXDNDecoderState(Channel channel, NXDNTrafficChannelManager trafficChannelManager)
+ {
+ mChannel = channel;
+ mTrafficChannelManager = trafficChannelManager;
+ }
+
+ /**
+ * Indicates if we have a non-null traffic channel manager
+ */
+ private boolean hasTrafficChannelManager()
+ {
+ return mTrafficChannelManager != null;
+ }
+
+ @Override
+ public void init()
+ {
+ }
+
+ @Override
+ public void start()
+ {
+ super.start();
+
+ //Change the default (45-second) traffic channel timeout to 1 second
+ if(mChannel.isTrafficChannel())
+ {
+ broadcast(new ChangeChannelTimeoutEvent(this, Channel.ChannelType.TRAFFIC, 1000));
+ }
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.NXDN;
+ }
+
+ @Override
+ public void receiveDecoderStateEvent(DecoderStateEvent event)
+ {
+ switch(event.getEvent())
+ {
+ case REQUEST_RESET:
+ resetState();
+ break;
+ case NOTIFICATION_SOURCE_FREQUENCY:
+ setCurrentFrequency(event.getFrequency());
+
+ //Only update the traffic channel manager if we're not a traffic channel.
+ if(hasTrafficChannelManager() && mChannel.isStandardChannel())
+ {
+ mTrafficChannelManager.setCurrentControlFrequency(getCurrentFrequency(), mChannel);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public String getActivitySummary()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("NXDN Channel Activity Summary\n");
+
+ if(mChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
+ {
+ sb.append("Transmission Mode: ").append(configNXDN.getTransmissionMode()).append("\n");
+ }
+ sb.append(mNetworkConfigurationMonitor.getSummary()).append("\n");
+ sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()).append("\n");
+ return sb.toString();
+ }
+
+ @Override
+ public void receive(IMessage iMessage)
+ {
+ if(iMessage instanceof NXDNMessage nxdn && nxdn.isValid())
+ {
+ if(nxdn instanceof NXDNLayer3Message layer3)
+ {
+ processLayer3(layer3);
+ }
+ else if(nxdn instanceof Audio audio)
+ {
+ processAudio(audio);
+ }
+ else if(nxdn instanceof NXDNPacketMessage packetMessage)
+ {
+ processPacketMessage(packetMessage);
+ }
+ }
+ }
+
+ /**
+ * Broadcasts the arguments as a new decode event
+ * @param identifiers involved in the event
+ * @param timestamp of the message/event
+ * @param decodeEventType of the event
+ * @param details for the event
+ */
+ private void broadcastEvent(List identifiers, long timestamp, DecodeEventType decodeEventType, String details)
+ {
+ MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers);
+ mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic);
+ broadcast(NXDNDecodeEvent.builder(decodeEventType, timestamp)
+ .channel(getCurrentChannel())
+ .details(details)
+ .identifiers(mic)
+ .build());
+ }
+
+ /**
+ * Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifiers
+ * passing each identifier through the patch group manager to replace with a patch group if it exists
+ * @param identifiers to add to the collection copy
+ * @return collection
+ */
+ private MutableIdentifierCollection getMutableIdentifierCollection(List identifiers)
+ {
+ MutableIdentifierCollection requestCollection = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
+ requestCollection.remove(IdentifierClass.USER);
+
+ for(Identifier identifier: identifiers)
+ {
+ requestCollection.update(identifier);
+ }
+
+ return requestCollection;
+ }
+
+ /**
+ * Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifier
+ * passed through the patch group manager to replace with a patch group if it exists
+ * @param identifier to add to the collection copy
+ * @return collection
+ */
+ private MutableIdentifierCollection getMutableIdentifierCollection(Identifier identifier)
+ {
+ return getMutableIdentifierCollection(Collections.singletonList(identifier));
+ }
+
+ /**
+ * Process layer 3 messages
+ * @param layer3 message to process
+ */
+ private void processLayer3(NXDNLayer3Message layer3)
+ {
+ State state = layer3.getMessageType().isControl() ? State.CONTROL : State.ACTIVE;
+ DecoderStateEvent.Event event = DecoderStateEvent.Event.DECODE;
+
+ switch(layer3.getMessageType())
+ {
+ case CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE:
+ case TYPE_D_OUT_00_CC_CALL_RESPONSE:
+ if(layer3 instanceof VoiceCallResponse vcr)
+ {
+ //Controller responds to a voice call request with either a channel grant, or this message if
+ //it can't support the call or the called party is not available
+ broadcastEvent(vcr.getIdentifiers(), vcr.getTimestamp(), DecodeEventType.RESPONSE,
+ "VOICE CALL REQUEST - RESPONSE:" + vcr.getCause().toString());
+ }
+ break;
+ case TRAFFIC_OUT_01_CC_VOICE_CALL:
+ case TYPE_D_OUT_01_CC_VOICE_CALL:
+ if(layer3 instanceof VoiceCall vc)
+ {
+ mIdleDuringCallCount = 0;
+ getIdentifierCollection().update(vc.getIdentifiers());
+ mEncryptedCallStateDetermined = true;
+ mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted();
+ state = mEncryptedCall ? State.ENCRYPTED : State.CALL;
+ event = DecoderStateEvent.Event.START;
+ mTrafficChannelManager.processVoiceCall(vc, getCurrentChannel());
+ }
+ break;
+ case CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
+ case TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
+ if(layer3 instanceof VoiceCallReceptionRequest vcrr)
+ {
+ broadcastEvent(vcrr.getIdentifiers(), vcrr.getTimestamp(), DecodeEventType.PAGE,
+ "VOICE CALL RECEPTION REQUEST");
+ }
+ break;
+ case CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE:
+ if(layer3 instanceof VoiceCallConnectionResponse vccr)
+ {
+ //Controller responds to a voice call connection request for the called party with either a channel
+ //grant, or this message if it can't support the call or connect the called party
+ broadcastEvent(vccr.getIdentifiers(), vccr.getTimestamp(), DecodeEventType.RESPONSE,
+ "CALL CONNECTION REQUEST - RESPONSE:" + vccr.getCause().toString());
+ }
+ break;
+ case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
+ case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
+ state = State.CALL;
+ event = DecoderStateEvent.Event.CONTINUATION;
+ mIdleDuringCallCount = 0;
+ break;
+ case CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
+ case TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
+ case TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
+ if(layer3 instanceof VoiceCallAssignment vca)
+ {
+ //Decode event is created by the traffic channel manager
+ mTrafficChannelManager.processVoiceCallAssignment(vca);
+ }
+ break;
+ case CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
+ if(layer3 instanceof VoiceCallAssignmentDuplicateControl vcadc)
+ {
+ //This informs when there are 2-calls ongoing where a radio can participate in either call
+ mTrafficChannelManager.processVoiceCallAssignment(vcadc);
+ }
+ break;
+ case TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
+ if(layer3 instanceof VoiceCallAssignmentDuplicateTraffic vcadt)
+ {
+ //This informs when there are 2-calls ongoing where a radio can participate in either call
+ mTrafficChannelManager.processVoiceCallAssignment(vcadt);
+ }
+ break;
+ case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
+ case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE:
+ case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE:
+ mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
+ mEncryptedCallStateDetermined = false;
+ mEncryptedCall = false;
+ mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
+ //Only remove the FROM identifier, anticipating a continuation call for the same talkgroup
+ getIdentifierCollection().remove(Role.FROM);
+ break;
+ case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE:
+ if(layer3 instanceof DataCallResponse dcr)
+ {
+ //Controller responds to a data call request with either a channel grant, or this message if
+ //it can't support the call or the called party is not available
+ broadcastEvent(dcr.getIdentifiers(), dcr.getTimestamp(), DecodeEventType.RESPONSE,
+ "DATA CALL REQUEST - RESPONSE:" + dcr.getCause().toString());
+ }
+ break;
+ case TRAFFIC_OUT_09_CC_DATA_CALL_HEADER:
+ case TYPE_D_OUT_09_CC_DATA_CALL_HEADER:
+ state = State.DATA;
+ break;
+ case CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
+ if(layer3 instanceof DataCallReceptionRequest dcrr)
+ {
+ broadcastEvent(dcrr.getIdentifiers(), dcrr.getTimestamp(), DecodeEventType.PAGE,
+ "DATA CALL RECEPTION REQUEST");
+ }
+ break;
+ case TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
+ break;
+ case TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK:
+ case TYPE_D_OUT_11_CC_DATA_CALL_BLOCK:
+ state = State.DATA;
+ break;
+ case TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
+ case TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
+ break;
+ case CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
+ if(layer3 instanceof DataCallAssignmentDuplicateControl dcadc)
+ {
+ mTrafficChannelManager.processDataCallAssignment(dcadc);
+ }
+ break;
+ case TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
+ break;
+ case CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT:
+ if(layer3 instanceof DataCallAssignment dca)
+ {
+ mTrafficChannelManager.processDataCallAssignment(dca);
+ }
+ break;
+ case TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT:
+ break;
+ case TRAFFIC_OUT_15_CC_HEADER_DELAY:
+ case TYPE_D_OUT_15_CC_HEADER_DELAY:
+ break;
+ case CONTROL_OUT_16_CC_IDLE:
+ break;
+ case TRAFFIC_OUT_16_CC_IDLE:
+ case TYPE_D_OUT_16_CC_IDLE:
+ //Hack: on both 4800 and 9600 systems there can be instances during a call where they send IDLE frames
+ //We'll track when we're in a call and continue a call state until the sequence of IDLE messages
+ //exceeds a threshold and then the state will be flipped to ACTIVE from CALL.
+ mIdleDuringCallCount++;
+
+ if(mIdleDuringCallCount < IDLE_DURING_CALL_MAX_COUNT)
+ {
+ state = State.CALL;
+ }
+ else
+ {
+ mEncryptedCallStateDetermined = false;
+ mEncryptedCall = false;
+ }
+ break;
+ case CONTROL_OUT_17_CC_DISCONNECT:
+ //Ignore. If this happens on the current/active control channel, in a multi-channel configuration,
+ //the frequency rotation manager will simply move to the next channel.
+ break;
+ case TRAFFIC_OUT_17_CC_DISCONNECT:
+ case TYPE_D_OUT_17_CC_DISCONNECT:
+ mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
+ mEncryptedCallStateDetermined = false;
+ mEncryptedCall = false;
+ getIdentifierCollection().remove(IdentifierClass.USER);
+ mDisconnectResponseCount++;
+
+ //Delay channel teardown until after we receive at least 2x disconnect responses
+ if(mChannel.isTrafficChannel() && mDisconnectResponseCount >= 2)
+ {
+ //Signal that this traffic channel has ended.
+ event = DecoderStateEvent.Event.END;
+ state = State.FADE;
+ mDisconnectResponseCount = 0;
+ }
+ break;
+
+ //Broadcast messages
+ case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
+ case CONTROL_OUT_24_BC_SITE_INFORMATION:
+ case CONTROL_OUT_25_BC_SERVICE_INFORMATION:
+ case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
+ mNetworkConfigurationMonitor.process(layer3);
+ getIdentifierCollection().remove(IdentifierClass.USER);
+ break;
+ case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
+ mNetworkConfigurationMonitor.process(layer3);
+
+ if(layer3 instanceof ControlChannelInformation cci)
+ {
+ setCurrentChannel(cci.getChannel1());
+ }
+ break;
+ case TRAFFIC_OUT_24_BC_SITE_INFORMATION:
+ if(layer3 instanceof SiteInformation si)
+ {
+ //Update traffic channel manager so it can use this when allocation traffic channels.
+ mTrafficChannelManager.setChannelAccessInformation(si.getChannelAccessInformation());
+ }
+ //Deliberate fall through
+ case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
+ case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID:
+ case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION:
+ case TYPE_D_OUT_25_BC_SERVICE_INFORMATION:
+ case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
+ case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION:
+ mNetworkConfigurationMonitor.process(layer3);
+ break;
+ case CONTROL_OUT_32_MM_REGISTRATION_RESPONSE:
+ if(layer3 instanceof RegistrationResponse rr)
+ {
+ broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString());
+ }
+ break;
+ case TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE:
+ if(layer3 instanceof RegistrationResponseTypeD rr)
+ {
+ broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString());
+ }
+ break;
+ case CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
+ case TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
+ if(layer3 instanceof RegistrationClearResponse rcr)
+ {
+ broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.RESPONSE,
+ "CLEAR REGISTRATION REQUEST - RESPONSE:" + rcr.getCause());
+ }
+ break;
+ case CONTROL_OUT_35_MM_REGISTRATION_COMMAND:
+ if(layer3 instanceof RegistrationCommand rc)
+ {
+ broadcastEvent(rc.getIdentifiers(), rc.getTimestamp(), DecodeEventType.REGISTER,
+ rc.getRegistrationOption().isPriorityStation() ? "PRIORITY STATION" : "");
+ }
+ break;
+ case CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
+ if(layer3 instanceof GroupRegistrationResponse grr)
+ {
+ broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE,
+ "GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause());
+ }
+ break;
+ case TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
+ if(layer3 instanceof GroupRegistrationResponseTypeD grr)
+ {
+ broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE,
+ "GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause());
+ }
+ break;
+ case TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION:
+ if(layer3 instanceof AuthenticationParameterInformation api)
+ {
+ broadcastEvent(api.getIdentifiers(), api.getTimestamp(), DecodeEventType.REQUEST,
+ "AUTHENTICATE: " + api.getAuthenticationParameter());
+ }
+ break;
+ case CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
+ case TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
+ if(layer3 instanceof AuthenticationInquiryRequest air)
+ {
+ broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST,
+ "AUTHENTICATE:" + air.getAuthenticationParameter());
+ }
+ break;
+ case TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE:
+ if(layer3 instanceof AuthenticationInquiryResponse air)
+ {
+ broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.RESPONSE,
+ "AUTHENTICATION:" + air.getAuthenticationValue());
+ }
+ break;
+ case CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
+ case TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
+ if(layer3 instanceof AuthenticationInquiryRequestMultiSystem air)
+ {
+ broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST,
+ "AUTHENTICATE (MULTI-SYSTEM):" + air.getAuthenticationParameter());
+ }
+ break;
+ case TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
+ if(layer3 instanceof AuthenticationInquiryResponseMultiSystem airms)
+ {
+ broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp())
+ .channel(getCurrentChannel())
+ .identifiers(getMutableIdentifierCollection(airms.getIdentifiers()))
+ .details("AUTHENTICATION: " + airms.getAuthenticationValue())
+ .build());
+ }
+ break;
+ case TYPE_D_OUT_43_MM_DATA_WRITE_HEADER:
+ case TYPE_D_OUT_44_MM_DATA_WRITE_DATA:
+ state = State.DATA;
+ break;
+ case CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST:
+ case TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST:
+ case TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST:
+ if(layer3 instanceof StatusInquiryRequest sir)
+ {
+ broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.REQUEST,
+ "STATUS INQUIRY");
+ }
+ break;
+ case CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
+ case TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
+ case TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
+ if(layer3 instanceof StatusInquiryResponse sir)
+ {
+ broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.RESPONSE,
+ "STATUS INQUIRY:" + sir.getStatus() + ":" + sir.getStatusValue() + " CAUSE:" + sir.getCause());
+ }
+ break;
+ case CONTROL_OUT_50_CC_STATUS_REQUEST:
+ case TRAFFIC_OUT_50_CC_STATUS_REQUEST:
+ case TYPE_D_OUT_50_CC_STATUS_REQUEST:
+ if(layer3 instanceof StatusRequest sr)
+ {
+ broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST,
+ "STATUS REQUEST:" + sr.getStatus());
+ }
+ break;
+ case CONTROL_OUT_51_CC_STATUS_RESPONSE:
+ case TRAFFIC_OUT_51_CC_STATUS_RESPONSE:
+ case TYPE_D_OUT_51_CC_STATUS_RESPONSE:
+ if(layer3 instanceof StatusResponse sr)
+ {
+ broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST,
+ "STATUS RESPONSE:" + sr.getCause());
+ }
+ break;
+ case CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST:
+ case TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST:
+ case TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST:
+ if(layer3 instanceof RemoteControlRequest rcr)
+ {
+ broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST,
+ "REMOTE CONTROL REQUEST - PARAMETERS:" + rcr.getControlParameters());
+ }
+ break;
+ case CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
+ case TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
+ case TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
+ if(layer3 instanceof RemoteControlResponse rcr)
+ {
+ broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST,
+ "REMOTE CONTROL " + rcr.getControlCommand() + " " + rcr.getCause());
+ }
+ break;
+ case CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN:
+ if(layer3 instanceof RemoteControlRequestWithESN rcrwe)
+ {
+ broadcastEvent(rcrwe.getIdentifiers(), rcrwe.getTimestamp(), DecodeEventType.REQUEST,
+ "REMOTE CONTROL REQUEST WITH ESN - PARAMETERS:" + rcrwe.getAuthenticationParameter());
+ }
+ break;
+ case CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
+ case TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
+ case TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
+ if(layer3 instanceof ShortDataCallRequestHeader sdcrh)
+ {
+ broadcast(NXDNDecodeEvent.builder(DecodeEventType.REQUEST, layer3.getTimestamp())
+ .channel(getCurrentChannel())
+ .identifiers(getMutableIdentifierCollection(sdcrh.getIdentifiers()))
+ .details("SHORT DATA CALL")
+ .build());
+ state = State.DATA;
+ }
+ break;
+ case TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK:
+ case TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
+ state = State.DATA;
+ case CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
+ break;
+ case TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
+ case TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
+ state = State.DATA;
+ case CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
+ break;
+ case CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
+ case TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
+ case TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
+ if(layer3 instanceof ShortDataCallResponse sdcr)
+ {
+ broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp())
+ .channel(getCurrentChannel())
+ .identifiers(getMutableIdentifierCollection(sdcr.getIdentifiers()))
+ .details("SHORT DATA CALL: " + sdcr.getCause())
+ .build());
+ }
+ break;
+
+ case PROPRIETARY_FORM:
+ break;
+ case TALKER_ALIAS:
+ state = State.CALL;
+ event = DecoderStateEvent.Event.CONTINUATION;
+ break;
+
+ case TALKER_ALIAS_COMPLETE:
+ if(layer3 instanceof TalkerAliasComplete tac)
+ {
+ state = State.CALL;
+ event = DecoderStateEvent.Event.CONTINUATION;
+ var radio = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
+
+ if(radio instanceof RadioIdentifier ri)
+ {
+ mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), tac.getTalkerAlias(), ri, tac.getTimestamp());
+ }
+ }
+ break;
+
+ case TRAFFIC_IN_01_CC_VOICE_CALL:
+ state = State.CALL;
+ break;
+ case TRAFFIC_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE:
+ case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
+ state = State.CALL;
+ break;
+ case TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE:
+ case TRAFFIC_IN_09_CC_DATA_CALL_HEADER:
+ state = State.DATA;
+ break;
+ case TRAFFIC_IN_11_CC_DATA_CALL_BLOCK:
+ state = State.DATA;
+ break;
+ case TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE:
+ case TRAFFIC_IN_15_CC_HEADER_DELAY:
+ case TRAFFIC_IN_16_CC_IDLE:
+ case TRAFFIC_IN_17_CC_DISCONNECT_REQUEST:
+ case TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
+ case TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
+ case TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST:
+ case TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE:
+ case TRAFFIC_IN_50_CC_STATUS_REQUEST:
+ case TRAFFIC_IN_51_CC_STATUS_RESPONSE:
+ case TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST:
+ case TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE:
+ case TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
+ case TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK:
+ state = State.DATA;
+ break;
+ case TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
+ state = State.DATA;
+ break;
+
+ case UNKNOWN:
+ break;
+ }
+
+ broadcast(new DecoderStateEvent(this, event, state));
+ }
+
+ /**
+ * Process an audio message
+ * @param audio to process
+ */
+ private void processAudio(Audio audio)
+ {
+ mIdleDuringCallCount = 0;
+ State state = State.CALL;
+
+ if(mEncryptedCallStateDetermined && mEncryptedCall)
+ {
+ state = State.ENCRYPTED;
+ }
+
+ mTrafficChannelManager.processCallProgressUpdate(getCurrentChannel(), audio.getTimestamp());
+
+ broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, state));
+ }
+
+ /**
+ * Process packet-based messages
+ * @param message to process
+ */
+ private void processPacketMessage(NXDNPacketMessage message)
+ {
+ if(message instanceof GPS gps)
+ {
+ PlottableDecodeEvent plottableEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, gps.getTimestamp())
+ .identifiers(new IdentifierCollection(message.getPacketSequence().getHeader().getIdentifiers()))
+ .channel(getCurrentChannel())
+ .details(gps.getLocationFormatted() + " HDG: " + gps.getHeading() + " SPD:" + gps.getSpeed() +
+ " ELE:" + gps.getElevation())
+ .protocol(Protocol.NXDN)
+ .location(gps.getLocation())
+ .heading(gps.getHeading())
+ .speed(gps.getSpeed())
+ .build();
+ broadcast(plottableEvent);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java
new file mode 100644
index 00000000..37149794
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessage.java
@@ -0,0 +1,155 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
+import io.github.dsheirer.protocol.Protocol;
+
+/**
+ * Base NXDN message implementation
+ */
+public abstract class NXDNMessage implements IMessage
+{
+ protected static int OCTET_0 = 0;
+ protected static int OCTET_1 = 8;
+ protected static int OCTET_2 = 16;
+ protected static int OCTET_3 = 24;
+ protected static int OCTET_4 = 32;
+ protected static int OCTET_5 = 40;
+ protected static int OCTET_6 = 48;
+ protected static int OCTET_7 = 56;
+ protected static int OCTET_8 = 64;
+ protected static int OCTET_9 = 72;
+ protected static int OCTET_10 = 80;
+ protected static int OCTET_11 = 88;
+ protected static int OCTET_12 = 96;
+ protected static int OCTET_13 = 104;
+ protected static int OCTET_14 = 112;
+ protected static int OCTET_15 = 120;
+ protected static int OCTET_16 = 128;
+ protected static int OCTET_17 = 136;
+ protected static int OCTET_18 = 144;
+ protected static int OCTET_19 = 152;
+ protected static int OCTET_20 = 160;
+ protected final int mRAN;
+ protected final LICH mLICH;
+
+ private CorrectedBinaryMessage mMessage;
+ private long mTimestamp;
+ private boolean mValid = true;
+
+ /**
+ * Constructs an instance
+ *
+ * @param message with binary data
+ * @param timestamp for the message
+ */
+ public NXDNMessage(CorrectedBinaryMessage message, long timestamp, int ran, LICH lich)
+ {
+ mMessage = message;
+ mTimestamp = timestamp;
+ mRAN = ran;
+ mLICH = lich;
+ }
+
+ @Override
+ public long getTimestamp()
+ {
+ return mTimestamp;
+ }
+
+ /**
+ * Underlying binary message
+ */
+ public CorrectedBinaryMessage getMessage()
+ {
+ return mMessage;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return mValid;
+ }
+
+ /**
+ * Marks this message as valid or invalid.
+ *
+ * @param valid flag
+ */
+ public void setValid(boolean valid)
+ {
+ mValid = valid;
+ }
+
+ @Override
+ public Protocol getProtocol()
+ {
+ return Protocol.NXDN;
+ }
+
+ @Override
+ public int getTimeslot()
+ {
+ return 0;
+ }
+
+ /**
+ * Formatted message prefix indicating CRC status, RAN and RF Channel.
+ */
+ public StringBuilder getMessageBuilder()
+ {
+ StringBuilder sb = new StringBuilder();
+ if(!isValid())
+ {
+ sb.append("[CRC-ERROR] ");
+ }
+ sb.append(getLICH().getRFChannel().name());
+ if(hasRAN())
+ {
+ sb.append(" RAN:").append(getRAN());
+ }
+ sb.append(" ");
+ return sb;
+ }
+
+ public boolean hasRAN()
+ {
+ return mRAN != 0;
+ }
+
+ /**
+ * Radio access network (RAN) from the frame carrier
+ */
+ public int getRAN()
+ {
+ return mRAN;
+ }
+
+ /**
+ * Link Information Channel (LICH) from the frame carrier
+ */
+ public LICH getLICH()
+ {
+ return mLICH;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java
new file mode 100644
index 00000000..456f3ff1
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java
@@ -0,0 +1,251 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.message.SyncLossMessage;
+import io.github.dsheirer.module.decode.nxdn.layer1.Frame;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNHardSyncDetector;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector;
+import io.github.dsheirer.module.decode.nxdn.layer2.LICHTracker;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.protocol.Protocol;
+import io.github.dsheirer.sample.Listener;
+import java.util.Arrays;
+
+/**
+ * Provides message framing for the demodulated dibit stream. This framer is notified by an external sync detection
+ * process using the two syncDetected() methods below to indicate if the NID that follows the sync was correctly error
+ * detected and corrected. When the NID does not pass error correction, we use a PLACEHOLDER data unit ID to allow the
+ * uncertain message to assemble and then we'll inspect before and after data unit IDs and the quantity of captured
+ * dibits to make a best guess on what the assembled message represents.
+ */
+public class NXDNMessageFramer
+{
+ private static final int FRAME_LENGTH = 192;
+ private static final int SYNC_LENGTH = 10;
+ private static final int PAYLOAD_LENGTH = FRAME_LENGTH - SYNC_LENGTH;
+ private static final int SYNC_DETECTION_BIT_ERROR_THRESHOLD = 8;
+ private final Dibit[] mDibitBuffer = new Dibit[192];
+ private final LICHTracker mLICHTracker;
+ private final Listener mMessageListener;
+ private final NXDNHardSyncDetector mHardSyncDetector = new NXDNHardSyncDetector();
+ private final int mSymbolsPerSecond;
+ private final int mCoarseSyncLossThreshold;
+ private boolean mSyncDetected = false;
+ private int mDibitBufferPointer = 0;
+ private int mDibitSinceTimestampCounter = 0;
+ private int mDibitsSinceLastMessage = FRAME_LENGTH;
+ private long mReferenceTimestamp = 0;
+
+ /**
+ * Constructs an instance
+ * @param messageListener to receive framed messages
+ * @param transmissionMode to indicate how many symbols per second for accounting
+ */
+ public NXDNMessageFramer(Listener messageListener, TransmissionMode transmissionMode)
+ {
+ mMessageListener = messageListener;
+ mLICHTracker = new LICHTracker(transmissionMode);
+ mSymbolsPerSecond = transmissionMode.getSymbolRate();
+ mCoarseSyncLossThreshold = mSymbolsPerSecond + FRAME_LENGTH;
+ Arrays.fill(mDibitBuffer, Dibit.D00_PLUS_1);
+ }
+
+ /**
+ * Sets or updates the current dibit stream time from an incoming sample buffer.
+ * @param time to use as a reference timestamp.
+ */
+ public void setTimestamp(long time)
+ {
+ mReferenceTimestamp = time;
+ mDibitSinceTimestampCounter = 0;
+ }
+
+
+ /**
+ * Calculates the timestamp accurate to the currently received dibit.
+ * @return timestamp in milliseconds.
+ */
+ private long getTimestamp()
+ {
+ if(mReferenceTimestamp > 0)
+ {
+ return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / 4800);
+ }
+ else
+ {
+ mDibitSinceTimestampCounter = 0;
+ return System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Dispatches a currently assembling message once it's complete.
+ * @return true if any of the messages from the frame are valid.
+ */
+ private boolean dispatchMessage()
+ {
+ CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(PAYLOAD_LENGTH * 2);
+ //The buffer pointer is the next symbol after the completed message, ie start of sync
+ int bufferDibitPointer = mDibitBufferPointer + SYNC_LENGTH;
+ bufferDibitPointer %= FRAME_LENGTH;
+ int messageDibitPointer = 0;
+
+ Dibit dibit;
+ while(messageDibitPointer < PAYLOAD_LENGTH)
+ {
+ dibit = mDibitBuffer[bufferDibitPointer++];
+ cbm.add(dibit.getBit1(), dibit.getBit2());
+ bufferDibitPointer %= FRAME_LENGTH;
+ messageDibitPointer++;
+ }
+
+ boolean validMessage = false;
+ Frame frame = new Frame(cbm, getTimestamp(), mLICHTracker);
+
+ for(NXDNMessage message: frame.getMessages())
+ {
+ validMessage |= message.isValid();
+ dispatch(message);
+ }
+
+ mDibitsSinceLastMessage = 0;
+
+ return validMessage;
+ }
+
+ /**
+ * Tests if the previous 10 symbols in the dibit buffer match the sync pattern. This uses the
+ * buffer wrapping to determine if the just collected dibit sync sequence contains a sync pattern but was not
+ * triggered by the sync detector. This should only be used at the message interval when we anticipate arrival of
+ * the succeeding message.
+ *
+ * @return bit errors for the received dibit sequence against the sync pattern
+ */
+ private int checkSync()
+ {
+ long value = 0;
+ int dibitBufferPointer = mDibitBufferPointer - SYNC_LENGTH;
+
+ if(dibitBufferPointer < 0)
+ {
+ dibitBufferPointer += FRAME_LENGTH;
+ }
+
+ for(int x = 0; x < SYNC_LENGTH; x++)
+ {
+ value <<= 2;
+ value += mDibitBuffer[dibitBufferPointer++].getValue();
+ dibitBufferPointer %= FRAME_LENGTH;
+ }
+
+ return Long.bitCount(value ^ NXDNSyncDetector.SYNC_PATTERN);
+ }
+
+ /**
+ * Dispatches the message to an optional listener
+ * @param message to dispatch
+ */
+ private void dispatch(IMessage message)
+ {
+ if(mMessageListener != null)
+ {
+ mMessageListener.receive(message);
+ }
+ }
+
+ /**
+ * Notification that the sync pattern was detected. The most recently received dibit should be the last symbol
+ * in the sync pattern and the next dibit received should be the start of the frame content.
+ */
+ public void syncDetected()
+ {
+ //Ignore false/extra sync when we're already in sync detection
+ if(!mSyncDetected)
+ {
+ mSyncDetected = true;
+
+ //Update sync loss accounting when we have more/less than the expected sync pattern symbol count.
+ if(mDibitsSinceLastMessage > SYNC_LENGTH)
+ {
+ dispatch(new SyncLossMessage(getTimestamp(),
+ (mDibitsSinceLastMessage - SYNC_LENGTH) * 2, Protocol.NXDN));
+ mDibitsSinceLastMessage = SYNC_LENGTH;
+ }
+ else if(mDibitsSinceLastMessage < SYNC_LENGTH)
+ {
+ mDibitsSinceLastMessage = SYNC_LENGTH;
+ }
+ }
+ }
+
+ /**
+ * Process symbol decision without sync detection.
+ * @param symbol that was demodulated.
+ */
+ public boolean process(Dibit symbol)
+ {
+ //Check when we didn't get a sync detect, and count-wise, we're at the next frame's end-sync location
+ if(!mSyncDetected && mDibitsSinceLastMessage == SYNC_LENGTH)
+ {
+ int bitErrors = checkSync();
+
+ if(bitErrors <= SYNC_DETECTION_BIT_ERROR_THRESHOLD)
+ {
+ mSyncDetected = true;
+ }
+ }
+
+ mDibitBuffer[mDibitBufferPointer++] = symbol;
+ mDibitBufferPointer %= FRAME_LENGTH;
+ mDibitsSinceLastMessage++;
+ mDibitSinceTimestampCounter++;
+
+ if(mSyncDetected && mDibitsSinceLastMessage == FRAME_LENGTH)
+ {
+ mSyncDetected = false;
+ return dispatchMessage();
+ }
+ else if(mDibitsSinceLastMessage >= mCoarseSyncLossThreshold)
+ {
+ mDibitsSinceLastMessage -= mSymbolsPerSecond;
+ dispatch(new SyncLossMessage(getTimestamp(), mSymbolsPerSecond * 2, Protocol.NXDN));
+ }
+
+ return false;
+ }
+
+ /**
+ * Process symbol decision with (hard) sync detection
+ * @param symbol to process
+ */
+ public void processWithHardSyncDetection(Dibit symbol)
+ {
+ process(symbol);
+
+ if(mHardSyncDetector.process(symbol))
+ {
+ syncDetected();
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java
new file mode 100644
index 00000000..d7038107
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageProcessor.java
@@ -0,0 +1,206 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation;
+import io.github.dsheirer.module.decode.nxdn.layer2.SACCHAssembler;
+import io.github.dsheirer.module.decode.nxdn.layer2.SACCHFragment;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Idle;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataInitializationVector;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.UserData;
+import io.github.dsheirer.module.decode.nxdn.layer3.data.PacketSequenceAssembler;
+import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAlias;
+import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasAssembler;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
+import io.github.dsheirer.sample.Listener;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * NXDN Message Processor.
+ *
+ * Performs post-message creation processing and enrichment before the message is sent downstream.
+ */
+public class NXDNMessageProcessor implements Listener
+{
+ private final static Logger LOGGER = LoggerFactory.getLogger(NXDNMessageProcessor.class);
+ private ChannelAccessInformation mChannelAccessInformation;
+ private final Map mChannelFrequencyMap = new HashMap<>();
+ private final PacketSequenceAssembler mPacketSequenceAssembler = new PacketSequenceAssembler();
+ private final SACCHAssembler mSACCHAssembler = new SACCHAssembler();
+ private final TalkerAliasAssembler mTalkerAliasAssembler;
+
+ /**
+ * Sets channel preload information. This is used to preload a traffic channel with system channel information
+ * from the control channel when a traffic channel is allocated, on startup.
+ * @param data to preload
+ */
+ public void preload(NXDNChannelInfoPreloadData data)
+ {
+ ChannelInformation channelInformation = data.getData();
+ if(mChannelAccessInformation == null)
+ {
+ mChannelAccessInformation = channelInformation.getChannelAccessInformation();
+ }
+
+ for(ChannelFrequency channelFrequency: channelInformation.getChannelFrequencies())
+ {
+ if(!mChannelFrequencyMap.containsKey(channelFrequency.getChannel()))
+ {
+ mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency);
+ }
+ }
+ }
+
+ /**
+ * Downstream message listener
+ */
+ private Listener mMessageListener;
+
+ /**
+ * Constructs an instance
+ * @param configNXDN that optionally contains a channel map for channel mode systems
+ */
+ public NXDNMessageProcessor(DecodeConfigNXDN configNXDN)
+ {
+ for(ChannelFrequency channelFrequency: configNXDN.getChannelMap())
+ {
+ mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency);
+ }
+
+ mTalkerAliasAssembler = new TalkerAliasAssembler(configNXDN.getEncoding());
+ }
+
+ /**
+ * Processes the message for enrichment or reassembly of fragments and sends the enriched message and any additional
+ * messages that were created during the enrichment to the registered message listener.
+ * @param message to process
+ */
+ @Override
+ public void receive(IMessage message)
+ {
+ List additionalMessagesToDispatch = null;
+
+ if(message != null && message.isValid())
+ {
+ //Capture channel access information from Site Information message
+ if(message instanceof SiteInformation siteInformation)
+ {
+ mChannelAccessInformation = siteInformation.getChannelAccessInformation();
+ }
+
+ //Enrich messages that carry channel information with the channel access info.
+ if(mChannelAccessInformation != null && message instanceof IChannelInformationReceiver receiver)
+ {
+ receiver.receive(mChannelAccessInformation, mChannelFrequencyMap);
+ }
+
+ //SACCH fragments are either standalone 4/4, or a sequence of 4 in a superframe. Reassemble them.
+ if(message instanceof SACCHFragment fragment)
+ {
+ dispatch(message);
+ message = null;
+ //Publish the completed SACCH message to this method so that it can be processed correctly and dispatch
+ //the original fragment and nullify the message to ensure correct sequencing where fragment 4 is
+ //dispatched first, and then the assembled SACCH message.
+ receive(mSACCHAssembler.process(fragment));
+ }
+
+ if(message instanceof TalkerAlias fragment)
+ {
+ dispatch(message);
+ //Assign the completed alias to the message to dispatch below, to ensure proper message output sequence
+ message = mTalkerAliasAssembler.process(fragment);
+ }
+ else if(message instanceof Idle || message instanceof Disconnect)
+ {
+ //Clear any captured talker alias fragments at the end of the call or idle mode
+ mTalkerAliasAssembler.reset();
+ }
+ else if(message instanceof IPacketHeader header)
+ {
+ mPacketSequenceAssembler.process(header);
+ }
+ else if(message instanceof UserData userDataBlock)
+ {
+ additionalMessagesToDispatch = mPacketSequenceAssembler.process(userDataBlock);
+ }
+ else if(message instanceof ShortDataInitializationVector iv)
+ {
+ mPacketSequenceAssembler.process(iv);
+ }
+ }
+
+ dispatch(message);
+
+ if(additionalMessagesToDispatch != null)
+ {
+ for(NXDNMessage additional: additionalMessagesToDispatch)
+ {
+ dispatch(additional);
+ }
+ }
+ }
+
+ /**
+ * Post-process the message for frequency band details.
+ * @param message to post process and dispatch
+ */
+ private void dispatch(IMessage message)
+ {
+ if(message != null && mMessageListener != null)
+ {
+ mMessageListener.receive(message);
+ }
+ }
+
+ /**
+ * Prepares for disposal of this instance.
+ */
+ public void dispose()
+ {
+ mMessageListener = null;
+ }
+
+ /**
+ * Sets the message listener to receive the output from this processor.
+ * @param listener to receive output messages
+ */
+ public void setMessageListener(Listener listener)
+ {
+ mMessageListener = listener;
+ }
+
+ /**
+ * Clears a registered message listener.
+ */
+ public void removeMessageListener()
+ {
+ mMessageListener = null;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java
new file mode 100644
index 00000000..aa38f1c7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNNetworkConfigurationMonitor.java
@@ -0,0 +1,249 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformationTypeD;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.DigitalStationIDInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.FailureStatusInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Neighbor;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ServiceInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterIdle;
+import io.github.dsheirer.module.decode.nxdn.layer3.scch.SiteID;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Processes NXDN Layer 3 messages to assemble a snapshot of the site's broadcast configuration details
+ */
+public class NXDNNetworkConfigurationMonitor
+{
+ private ControlChannelInformation mControlChannelInformation;
+ private DigitalStationIDInformation mDigitalStationIDInformation;
+ private FailureStatusInformation mFailureStatusInformation;
+ private ServiceInformation mServiceInformation;
+ private SiteInformation mSiteInformation;
+ private Map mNeighborMap = new HashMap<>();
+
+ private AdjacentSiteInformationTypeD mTypeDNeighborA;
+ private AdjacentSiteInformationTypeD mTypeDNeighborB;
+ private Integer mTypeDRepeater;
+ private List mTypeDObservedRepeaters = new ArrayList<>();
+ private SiteID mTypeDSiteID;
+
+ /**
+ * Constructs an instance
+ */
+ public NXDNNetworkConfigurationMonitor()
+ {
+ }
+
+ public String getSummary()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(mDigitalStationIDInformation != null)
+ {
+ sb.append("Current Channel Station ID\n ").append(mDigitalStationIDInformation).append("\n");
+ }
+ if(mSiteInformation != null)
+ {
+ sb.append("\nCurrent Site\n ").append(mSiteInformation).append("\n");
+ }
+ else if(mTypeDSiteID != null)
+ {
+ sb.append("\nCurrent Site\n ").append(mTypeDSiteID).append("\n");
+ }
+ if(mServiceInformation != null)
+ {
+ sb.append("\nCurrent Site Services\n ").append(mServiceInformation).append("\n");
+ }
+ if(mFailureStatusInformation != null)
+ {
+ sb.append("\nFailure Status\n ").append(mFailureStatusInformation).append("\n");
+ }
+ if(mControlChannelInformation != null)
+ {
+ sb.append("\nCurrent Site Control Channel\n ").append(mControlChannelInformation).append("\n");
+ }
+
+ if(mTypeDRepeater != null)
+ {
+ sb.append("\nType-D Current Repeater: ").append(mTypeDRepeater).append("\n");
+ }
+
+ if(!mTypeDObservedRepeaters.isEmpty())
+ {
+ Collections.sort(mTypeDObservedRepeaters);
+ sb.append("\nType-D Observed Repeater Numbers:").append(mTypeDObservedRepeaters).append("\n");
+ }
+
+ if(!mNeighborMap.isEmpty())
+ {
+ sb.append("\nNeighbor Sites\n");
+ List ids = new ArrayList<>(mNeighborMap.keySet());
+ Collections.sort(ids);
+
+ for(Integer id : ids)
+ {
+ sb.append(" ").append(mNeighborMap.get(id)).append("\n");
+ }
+ }
+ else if(mTypeDNeighborA != null || mTypeDNeighborB != null)
+ {
+ sb.append("\nNeighbor Sites\n");
+
+ if(mTypeDNeighborA != null)
+ {
+ sb.append(" ").append(mTypeDNeighborA.getSystemID1()).append(" SITE:").append(mTypeDNeighborA.getSite1()).append("\n");
+
+ if(mTypeDNeighborA.hasSite2())
+ {
+ sb.append(" ").append(mTypeDNeighborA.getSystemID2()).append(" SITE:").append(mTypeDNeighborA.getSite2()).append("\n");
+ }
+ }
+ if(mTypeDNeighborB != null)
+ {
+ sb.append(" ").append(mTypeDNeighborB.getSystemID1()).append(" SITE:").append(mTypeDNeighborB.getSite1()).append("\n");
+
+ if(mTypeDNeighborB.hasSite2())
+ {
+ sb.append(" ").append(mTypeDNeighborB.getSystemID2()).append(" SITE:").append(mTypeDNeighborB.getSite2()).append("\n");
+ }
+ }
+ }
+ else
+ {
+ sb.append("\nNeighbor Sites\n NONE\n");
+ }
+
+ return sb.toString();
+ }
+
+ public void process(NXDNLayer3Message layer3)
+ {
+ switch(layer3.getMessageType())
+ {
+ case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
+ case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
+ case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID:
+ if(layer3 instanceof DigitalStationIDInformation dsii)
+ {
+ mDigitalStationIDInformation = dsii;
+ }
+ break;
+ case CONTROL_OUT_24_BC_SITE_INFORMATION:
+ case TRAFFIC_OUT_24_BC_SITE_INFORMATION:
+ mSiteInformation = (SiteInformation) layer3;
+ break;
+ case CONTROL_OUT_25_BC_SERVICE_INFORMATION:
+ case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION:
+ case TYPE_D_OUT_25_BC_SERVICE_INFORMATION:
+ if(layer3 instanceof ServiceInformation sii)
+ {
+ mServiceInformation = sii;
+ }
+ break;
+ case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
+ case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
+ mControlChannelInformation = (ControlChannelInformation) layer3;
+ break;
+ case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
+ case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION:
+ mFailureStatusInformation = (FailureStatusInformation) layer3;
+ break;
+ case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ if(layer3 instanceof AdjacentSiteInformation adjacent)
+ {
+ if(adjacent.hasChannel1())
+ {
+ Neighbor n1 = adjacent.getNeighbor1();
+
+ if(n1 != null)
+ {
+ mNeighborMap.put(n1.id(), n1);
+
+ if(adjacent.hasChannel2())
+ {
+ Neighbor n2 = adjacent.getNeighbor2();
+
+ if(n2 != null)
+ {
+ mNeighborMap.put(n2.id(), n2);
+
+ if(adjacent.hasChannel3())
+ {
+ Neighbor n3 = adjacent.getNeighbor3();
+
+ if(n3 != null)
+ {
+ mNeighborMap.put(n3.id(), n3);
+
+ if(adjacent.hasChannel4())
+ {
+ Neighbor n4 = adjacent.getNeighbor4();
+
+ if(n4 != null)
+ {
+ mNeighborMap.put(n4.id(), n4);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION:
+ if(layer3 instanceof AdjacentSiteInformationTypeD atd)
+ {
+ if(atd.isIndex())
+ {
+ mTypeDNeighborB = atd;
+ }
+ else
+ {
+ mTypeDNeighborA = atd;
+ }
+ }
+ break;
+ case TYPE_D_SCCH_OUT_INFO_4_REPEATER_IDLE:
+ if(layer3 instanceof RepeaterIdle ri)
+ {
+ mTypeDRepeater = ri.getRepeater();
+
+ if(!mTypeDObservedRepeaters.contains(ri.getRepeater2()))
+ {
+ mTypeDObservedRepeaters.add(ri.getRepeater2());
+ }
+ }
+ break;
+ case TYPE_D_SCCH_OUT_INFO_4_SITE_ID:
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java
new file mode 100644
index 00000000..c008d6e2
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java
@@ -0,0 +1,1123 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
+import io.github.dsheirer.gui.viewer.sync.FMSyncResultsViewer;
+import io.github.dsheirer.gui.viewer.sync.ISyncResultsListener;
+import io.github.dsheirer.module.decode.FeedbackDecoder;
+import io.github.dsheirer.module.decode.nxdn.layer1.C4FMEqualizer;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSoftSyncDetector;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetectorFactory;
+import io.github.dsheirer.module.decode.nxdn.layer1.sync.ReferenceSyncWaveform;
+import io.github.dsheirer.sample.Listener;
+import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import org.apache.commons.math3.stat.descriptive.moment.Mean;
+import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
+
+/**
+ * NXDN symbol processor provides symbol timing recovery and sync detection.
+ */
+public class NXDNSymbolProcessor
+{
+ private static final float TIMING_LOOP_GAIN = 0.4f;
+ private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
+ private static final float SYNC_THRESHOLD_DETECTION = 37;
+ private static final float SYNC_THRESHOLD_OPTIMIZED = 40;
+ private static final float SYNC_THRESHOLD_EQUALIZED = 40;
+ private static final float TWO_PI = (float)(Math.PI * 2.0);
+ private static final int BUFFER_WORKSPACE_LENGTH = 1024;
+ private static final int DIBIT_LENGTH_NID = 33; //32 dibits (64 bits) +1 status
+ private static final int DIBIT_LENGTH_SYNC = 10;
+ private static final Correction INVALID_SYNC_DETECTION = new Correction(0d, Double.MAX_VALUE, 0f, 0f, 0f, 0f, 0);
+ private ISyncResultsListener mSyncResultsViewer;
+ private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300);
+ private final FeedbackDecoder mFeedbackDecoder;
+ private final NXDNMessageFramer mMessageFramer;
+ private final NXDNSoftSyncDetector mSyncDetector = NXDNSyncDetectorFactory.getStandardDetector();
+ private final NXDNSoftSyncDetector mSyncDetectorLagging = NXDNSyncDetectorFactory.getStandardDetector();
+ private final SampleEqualizer mSampleEqualizer = new SampleEqualizer();
+ private final C4FMEqualizer mSymbolEqualizer;
+ private final SyncTracker mSyncTracker = new SyncTracker(2400, 3);
+ private double mNoiseStandardDeviationThreshold;
+ private double mSamplePoint;
+ private double mSamplePointAdjustment;
+ private double mSamplePointAdjustmentIncrement;
+ private double mSamplePointAdjustmentMax;
+ private double mSamplesPerSymbol;
+ private double mTimingCorrectionThreshold;
+ private float[] mBuffer;
+ private float mLaggingSyncOffset;
+ private float mOptimizeFineIncrement;
+ private int mBufferPointer;
+ private int mBufferReloadThreshold;
+ private int mSymbolsSinceLastSync = 0;
+ private int mDebugSymbolCounter = 0;
+ private int mDebugSampleCounter = 0;
+ private boolean mSynchronized = false;
+ private boolean mDelayedSyncNotificationPending = false;
+ private int mDelayedSyncNotificationSymbolsRemaining = 0;
+ private ReferenceSyncWaveform mReferenceSyncWaveform;
+ private final boolean mVisualize = false;
+ private final int mDebugSymbolStart = 200;
+ private static final DecimalFormat DF = new DecimalFormat("00.000");
+
+
+ /**
+ * Constructs an instance
+ * @param messageFramer to receive symbol decisions (dibits) and sync notifications.
+ */
+ public NXDNSymbolProcessor(NXDNMessageFramer messageFramer, FeedbackDecoder feedbackDecoder)
+ {
+ mMessageFramer = messageFramer;
+ mFeedbackDecoder = feedbackDecoder;
+ mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getSyncSymbols(), 9, 192 * 3);
+ DF.setPositivePrefix(" ");
+ }
+
+ public void resetEqualizer()
+ {
+ mSymbolEqualizer.reset();
+ }
+
+ public String getEqualizerSettings()
+ {
+ return mSymbolEqualizer.getTapsDescription();
+ }
+
+
+ public void log()
+ {
+ System.out.println("SAMPLES: " + mDebugSampleCounter + " SYMBOLS: " + mDebugSymbolCounter);
+ }
+
+ /**
+ * Finalize and prepare for shutdown.
+ */
+ public void dispose()
+ {
+ mDibitAssembler.flush();
+ }
+
+ /**
+ * Resets the balance
+ */
+ public void resetBalance()
+ {
+ mSampleEqualizer.reset();
+ }
+
+ /**
+ * Primary input method for receiving a stream of demodulated samples to process into symbols.
+ * @param samples to process
+ */
+ public void process(float[] samples)
+ {
+ //Create shadow copy of heap variables on stack
+ double samplePoint = mSamplePoint;
+ double samplesPerSymbol = mSamplesPerSymbol;
+ int bufferPointer = mBufferPointer;
+ int bufferReloadThreshold = mBufferReloadThreshold;
+ int symbolsSinceLastSync = mSymbolsSinceLastSync;
+
+ int samplesPointer = 0;
+ float softSymbol, scorePrimary;
+ Correction correctionCandidate;
+
+ if(!mSynchronized && samples.length > 400)
+ {
+ StandardDeviation sd = new StandardDeviation();
+
+ float accumulator = 0.0f;
+
+ for(int i = 0; i < 400; i++)
+ {
+ accumulator += samples[i];
+ sd.increment(samples[i]);
+ }
+
+ //Only apply a balance correction when we have signal. Noise = .45-.55, Signal = 0.00 - 0.20
+ if(sd.getResult() < 0.3)
+ {
+ accumulator /= 400;
+ accumulator += mSampleEqualizer.mBalance;
+ mSampleEqualizer.mBalance -= (accumulator * .3f);
+ }
+ }
+
+ while(samplesPointer < samples.length)
+ {
+ //Note: buffer pointer can become greater than reload threshold during timing optimization at sync detect
+ if(bufferPointer >= bufferReloadThreshold)
+ {
+ //Do reload
+ int copyLength = Math.min(BUFFER_WORKSPACE_LENGTH, samples.length - samplesPointer);
+ System.arraycopy(mBuffer, copyLength, mBuffer, 0, mBuffer.length - copyLength);
+ System.arraycopy(samples, samplesPointer, mBuffer, mBuffer.length - copyLength, copyLength);
+ samplesPointer += copyLength;
+ bufferPointer -= copyLength;
+ }
+
+ while(bufferPointer < bufferReloadThreshold)
+ {
+ bufferPointer++;
+ samplePoint--;
+
+ mDebugSampleCounter++;
+
+ if(samplePoint < 1)
+ {
+ mDebugSymbolCounter++;
+
+ if(mDebugSymbolCounter % 10 == 0)
+ {
+ visualizeSymbols(bufferPointer, samplePoint);
+ }
+
+ symbolsSinceLastSync++;
+
+ //Clear synchronized state when we miss a sync period.
+ if(mSynchronized && symbolsSinceLastSync > 192)
+ {
+ mSampleEqualizer.reset();
+ mSymbolEqualizer.syncLost();
+ mSynchronized = false;
+ }
+
+ softSymbol = mSampleEqualizer.getEqualizedSymbol(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], samplePoint);
+
+ //Check for lagging sync pattern
+ float lag = (float)(bufferPointer + samplePoint - mLaggingSyncOffset);
+ int lagIntegral = (int)Math.floor(lag);
+ float softSymbolLag = mSampleEqualizer.getEqualizedSymbol(mBuffer[lagIntegral], mBuffer[lagIntegral + 1], lag - lagIntegral);
+ float scoreLag = mSyncDetectorLagging.process(softSymbolLag);
+ scorePrimary = mSyncDetector.process(softSymbol);
+
+ correctionCandidate = INVALID_SYNC_DETECTION;
+
+ if(scorePrimary > SYNC_THRESHOLD_DETECTION && scorePrimary > scoreLag)
+ {
+ correctionCandidate = mSampleEqualizer.optimize(mSyncDetector, 0, scorePrimary,
+ (bufferPointer + samplePoint));
+ }
+ else if(scoreLag > SYNC_THRESHOLD_DETECTION)
+ {
+ correctionCandidate = mSampleEqualizer.optimize(mSyncDetectorLagging, -mLaggingSyncOffset,
+ scoreLag, (bufferPointer + samplePoint));
+ }
+
+ softSymbol = mSymbolEqualizer.process(softSymbolLag, softSymbol);
+
+ //Broadcast the decoded soft symbol to an optionally registered listener (ie Symbol view in channel panel).
+ Dibit symbol = toSymbol(softSymbol);
+
+ //Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized
+ mFeedbackDecoder.broadcast(softSymbol);
+
+ if(mVisualize)
+ {
+ getSyncResultsViewer().symbol(softSymbol);
+ }
+
+ //When the message framer emits at least one valid message (ie CRC check passes), send the current
+ // sample equalizer's balance value as a pseudo-PLL error measurement.
+ if(mMessageFramer.process(symbol))
+ {
+ mFeedbackDecoder.processPLLError(mSampleEqualizer.balance());
+ }
+
+ mDibitAssembler.receive(symbol);
+
+ //Do a final validation against the reference sync waveform
+ if(correctionCandidate.isValid())
+ {
+ correctionCandidate = validateSyncWaveform(correctionCandidate, bufferPointer, samplePoint);
+ }
+
+ if(correctionCandidate.isValid())
+ {
+ mSampleEqualizer.apply(correctionCandidate);
+
+ if(mSynchronized)
+ {
+ double adjustment = Math.clamp(correctionCandidate.getTimingCorrection(),
+ -mTimingCorrectionThreshold, mTimingCorrectionThreshold);
+ samplePoint += adjustment;
+ }
+ else
+ {
+ samplePoint += correctionCandidate.getTimingCorrection();
+ }
+
+ mDelayedSyncNotificationPending = true;
+ mDelayedSyncNotificationSymbolsRemaining = mSymbolEqualizer.getDelaySymbolCount();
+ mSynchronized = true;
+ symbolsSinceLastSync = 0;
+
+ //If the equalizer was disabled, it has bad samples contaminating the training buffer and we
+ // need to prime that buffer with aligned samples prior to sync detection training.
+ if(!mSymbolEqualizer.isEnabled())
+ {
+ int symbols = mSyncDetector.getSyncSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount();
+ double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset;
+
+ float mid, sym;
+ for(int s = 0; s < symbols; s++)
+ {
+ //Mid symbol
+
+ mid = mSampleEqualizer.getEqualizedSymbol(offset);
+ offset += mLaggingSyncOffset;
+ sym = mSampleEqualizer.getEqualizedSymbol(offset);
+ offset += mLaggingSyncOffset;
+ mSymbolEqualizer.process(mid, sym);
+ }
+ }
+
+ mSymbolEqualizer.syncDetected();
+ mSymbolEqualizer.enable();
+ visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint);
+ }
+ else if(mDelayedSyncNotificationPending)
+ {
+ mDelayedSyncNotificationSymbolsRemaining--;
+
+ if(mDelayedSyncNotificationSymbolsRemaining <= 0)
+ {
+ mMessageFramer.syncDetected();
+ mDelayedSyncNotificationPending = false;
+ }
+ }
+
+ //Add another symbol's worth of samples to the counter for processing
+ samplePoint += samplesPerSymbol;
+ }
+ }
+ }
+
+ //Copy shadow variables back to the heap
+ mBufferPointer = bufferPointer;
+ mSamplePoint = samplePoint;
+ mSymbolsSinceLastSync = symbolsSinceLastSync;
+ }
+
+ /**
+ * Validates the sync detection and candidate correction settings against a reference instance of the sync detection
+ * waveform. If the detected sync correlates well against the reference sync then the correction is valid.
+ * Otherwise, this method returns the static invalid sync correction to signal that the sync detection was invalid.
+ * @param correction to evaluate
+ * @param bufferPointer for the final (optimized) symbol in the buffer
+ * @param samplePoint offset from the buffer pointer to the final symbol
+ * @return the original correction or the static invalid sync correction
+ */
+ private Correction validateSyncWaveform(Correction correction, int bufferPointer, double samplePoint)
+ {
+ float gain = mSampleEqualizer.mGain + correction.getGainAdjustment();
+ float balance = mSampleEqualizer.mBalance + correction.getBalanceAdjustment();
+ double offset = bufferPointer + samplePoint + correction.getTimingCorrection() - mReferenceSyncWaveform.samples().length;
+ int integral = (int)Math.ceil(offset);
+ double mu = integral - offset;
+ double accumulator = 0.0;
+ int index;
+
+ //Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample
+ for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--)
+ {
+ index = integral + x;
+ float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu);
+ sample += balance;
+ sample *= gain;
+ sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample());
+ accumulator += mReferenceSyncWaveform.samples()[x] * sample;
+ }
+
+ double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
+
+ //Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static
+ // invalid sync detection to nullify the correction.
+ if(accumulator > threshold)
+ {
+ return correction;
+ }
+
+ return INVALID_SYNC_DETECTION;
+ }
+
+ /**
+ * Registers the listener to receive demodulated bit stream buffers.
+ * @param listener to register
+ */
+ public void setBufferListener(Listener listener)
+ {
+ mDibitAssembler.setBufferListener(listener);
+ }
+
+ /**
+ * Indicates if there is a registered buffer listener
+ */
+ public boolean hasBufferListener()
+ {
+ return mDibitAssembler.hasBufferListeners();
+ }
+
+ /**
+ * Sets or updates the samples per symbol
+ * @param samplesPerSymbol to apply.
+ */
+ public void setSamplesPerSymbol(float samplesPerSymbol)
+ {
+ mSamplesPerSymbol = samplesPerSymbol;
+ mTimingCorrectionThreshold = samplesPerSymbol / 30;
+ mNoiseStandardDeviationThreshold = .2;
+ mSamplePoint = samplesPerSymbol;
+ mSamplePointAdjustmentMax = samplesPerSymbol / 2;
+ mSamplePointAdjustmentIncrement = samplesPerSymbol / 100.0;
+ mOptimizeFineIncrement = samplesPerSymbol / 200f;
+ mLaggingSyncOffset = samplesPerSymbol / 2;
+ int bufferLength = BUFFER_WORKSPACE_LENGTH + (int)(Math.ceil((DIBIT_LENGTH_SYNC + DIBIT_LENGTH_NID + 2) * samplesPerSymbol));
+ mBuffer = new float[bufferLength];
+ mBufferReloadThreshold = mBuffer.length - (int)Math.ceil(samplesPerSymbol * (DIBIT_LENGTH_NID + 1));
+ mBufferPointer = mBufferReloadThreshold;
+ mSampleEqualizer.configure(samplesPerSymbol);
+ mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol);
+ }
+
+ /**
+ * Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
+ * @param sample in radians.
+ * @return symbol decision.
+ */
+ public static Dibit toSymbol(float sample)
+ {
+ if(sample > 0)
+ {
+ return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
+ }
+ else
+ {
+ return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
+ }
+ }
+
+ /**
+ * Debug viewer UI for visualizing sync detections in the sample stream/waveform.
+ */
+ private ISyncResultsListener getSyncResultsViewer()
+ {
+ if(mSyncResultsViewer == null)
+ {
+ mSyncResultsViewer = new FMSyncResultsViewer();
+ }
+
+ return mSyncResultsViewer;
+ }
+
+ /**
+ * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
+ * @param score from the sync detector
+ * @param primary sync detector (true) or secondary (false)
+ */
+ public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
+ {
+ if(!mVisualize)
+ {
+ return;
+ }
+
+// //Correct for the 1-symbol delay from the symbol equalizer
+// samplePoint -= mSamplesPerSymbol;
+ int integral = (int)Math.floor(samplePoint);
+ samplePoint -= integral;
+ bufferPointer += integral;
+
+ int offset = 3;
+ int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset);
+ int end = bufferPointer + offset;
+ int start = end - length;
+ float[] symbols = new float[10];
+ float[] samples = Arrays.copyOfRange(mBuffer, start, end);
+ for(int i = 0; i < samples.length; i++)
+ {
+ samples[i] = mSampleEqualizer.equalize(samples[i]);
+ }
+
+ double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9);
+ int symbolIntegral = (int)Math.floor(symbolPointer);
+ double mu = symbolPointer - symbolIntegral;
+
+ for(int x = 0; x < 10; x++)
+ {
+ symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
+ symbolPointer += mSamplesPerSymbol;
+ symbolIntegral = (int)Math.floor(symbolPointer);
+ mu = symbolPointer - symbolIntegral;
+ }
+
+ float[] syncIntervals = new float[10];
+ int adjust = bufferPointer - length + offset;
+ double pointer = bufferPointer + samplePoint - adjust;
+
+ for(int x = 9; x >= 0; x--)
+ {
+ syncIntervals[x] = (float)pointer;
+ pointer -= mSamplesPerSymbol;
+ }
+
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ getSyncResultsViewer().receive(symbols, mSyncDetector.getSyncSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
+ tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") +
+ " EQ-B:" + mSampleEqualizer.mBalance + " EQ-G:" + mSampleEqualizer.mGain + " " +
+ " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0),
+ countDownLatch);
+
+ try
+ {
+ countDownLatch.await();
+ }
+ catch(InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Debug method to visualize the symbol decisions
+ */
+ public void visualizeSymbols(int bufferPointer, double samplePoint)
+ {
+ if(!mVisualize)
+ {
+ return;
+ }
+
+ if(mDebugSymbolCounter < mDebugSymbolStart)
+ {
+ return;
+ }
+
+ int offset = 3;
+ int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset);
+ int end = bufferPointer + offset;
+ int start = end - length;
+ float[] symbols = new float[10];
+ float[] decisions = new float[10];
+ float[] samples = Arrays.copyOfRange(mBuffer, start, end);
+ for(int i = 0; i < samples.length; i++)
+ {
+ samples[i] = mSampleEqualizer.equalize(samples[i]);
+ }
+
+ double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9);
+ int symbolIntegral = (int)Math.floor(symbolPointer);
+ double mu = symbolPointer - symbolIntegral;
+
+ for(int x = 0; x < 10; x++)
+ {
+ symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
+ decisions[x] = toSymbol(symbols[x]).getIdealPhase();
+ symbolPointer += mSamplesPerSymbol;
+ symbolIntegral = (int)Math.floor(symbolPointer);
+ mu = symbolPointer - symbolIntegral;
+ }
+
+ float[] syncIntervals = new float[10];
+ int adjust = bufferPointer - length + offset;
+ double pointer = bufferPointer + samplePoint - adjust;
+
+ for(int x = 9; x >= 0; x--)
+ {
+ syncIntervals[x] = (float)pointer;
+ pointer -= mSamplesPerSymbol;
+ }
+
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
+ "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain +
+ " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), countDownLatch);
+
+ try
+ {
+ countDownLatch.await();
+ }
+ catch(InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Indicates if the samples in the sample buffer that contain a detected sync pattern have a standard deviation
+ * that is lower than the expected sample to sample deviation for a modulated signal. False detects against
+ * noise tend to have a standard deviation that is 2x the expected value.
+ * @param offset to the sample representing the final symbol in the detected sync pattern.
+ * @return true if the standard deviation is more than expected.
+ */
+ public double getStandardDeviation(double offset)
+ {
+ StandardDeviation standardDeviation = new StandardDeviation();
+ int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol));
+ int end = (int)Math.ceil(offset);
+ end = Math.min(end, mBuffer.length - 1);
+
+ for(int i = start; i < end; i++)
+ {
+ standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]);
+ }
+
+ return standardDeviation.getResult();
+ }
+
+ /**
+ * Symbol timing and equalizer correction candidate settings
+ */
+ public static class Correction
+ {
+ private final double mTimingAdjustment;
+ private final double mAdditionalOffset;
+ private final float mBalanceAdjustment;
+ private final float mGainAdjustment;
+ private final float mDetectionScore;
+ private final float mOptimizationScore;
+ private int mCorrectedBitCount = 0;
+
+ /**
+ * Constructs an instance
+ * @param timingAdjustment correction for symbol sampling
+ * @param balanceAdjustment correction for equalizer
+ * @param gainAdjustment correction for equalizer
+ * @param bitErrorCount in the detected sync pattern
+ */
+ public Correction(double additionalOffset, double timingAdjustment, float balanceAdjustment, float gainAdjustment,
+ float detectionScore, float optimizationScore, int bitErrorCount)
+ {
+ mAdditionalOffset = additionalOffset;
+ mTimingAdjustment = timingAdjustment;
+ mBalanceAdjustment = balanceAdjustment;
+ mGainAdjustment = gainAdjustment;
+ mDetectionScore = detectionScore;
+ mOptimizationScore = optimizationScore;
+ mCorrectedBitCount = bitErrorCount;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(isValid())
+ {
+ sb.append("\tCORRECTION - Bit Errors [").append(mCorrectedBitCount).append("]");
+ }
+ else
+ {
+ sb.append("CORRECTION - INVALID");
+ }
+
+ sb.append(" At Offset [").append(mAdditionalOffset);
+ sb.append("] Adjustments-Timing [").append(mTimingAdjustment);
+ sb.append("] Balance [").append(mBalanceAdjustment);
+ sb.append("] Gain [").append(mGainAdjustment);
+ sb.append("] Detect Score [").append(mDetectionScore);
+ sb.append("] Optimize Score [").append(mOptimizationScore);
+ sb.append("]");
+
+ if(!hasValidTiming())
+ {
+ sb.append(" >> INVALID TIMING");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Candidate timing correction
+ * @return correction
+ */
+ public double getTimingCorrection()
+ {
+ return mAdditionalOffset + mTimingAdjustment;
+ }
+
+ /**
+ * Equalizer balance correction value.
+ * @return correction
+ */
+ public float getBalanceAdjustment()
+ {
+ return mBalanceAdjustment;
+ }
+
+ /**
+ * Equalizer gain correction value
+ * @return correction.
+ */
+ public float getGainAdjustment()
+ {
+ return mGainAdjustment;
+ }
+
+ /**
+ * Number of bits that were corrected across the SYNC and NID
+ * @return corrected bit count.
+ */
+ public int getCorrectedBitCount()
+ {
+ return mCorrectedBitCount;
+ }
+
+ /**
+ * Indicates if this correction has valid timing adjustment information.
+ * @return true if the timing correction is not the default (ie no timing) double max value.
+ */
+ public boolean hasValidTiming()
+ {
+ return getTimingCorrection() != Double.MAX_VALUE;
+ }
+
+ /**
+ * Indicates if this correction has a valid NID or the optimization (or detection) score is high quality.
+ * @return true if valid.
+ */
+ public boolean isValid()
+ {
+ return mOptimizationScore > SYNC_THRESHOLD_EQUALIZED;
+ }
+ }
+
+ /**
+ * Sample equalizer providing gain and balance (DC offset compensation).
+ *
+ * Note: equalizer is initialized with a gain value of 1.219, as determined through significant testing against
+ * both P25 and DMR C4FM modulated samples that employ pulse shaping filters. Gain value is adjusted and
+ * optimized with each sync detection.
+ */
+ class SampleEqualizer
+ {
+ private static final float EQUALIZER_LOOP_GAIN = 0.15f;
+ private static final float EQUALIZER_MAXIMUM_BALANCE = (float)(Math.PI / 3.0); //+/- 800 Hz
+ private static final float EQUALIZER_RECALIBRATE_THRESHOLD = (float)(Math.PI / 8.0);
+ private float mBalance = 0.0f;
+ private float mGain = 5.7f;
+ private float mGainInitial = 5.7f;
+ private float mGainMax = 6.2f;
+ private float mGainMin = 5.2f;
+
+ /**
+ * Current balance that represents a traditional PLL signal offset value.
+ * @return balance.
+ */
+ public float balance()
+ {
+ return mBalance;
+ }
+
+ /**
+ * Updates the initial, min and max gain values from the samples per symbol
+ * @param samplesPerSymbol for the incoming stream
+ */
+ public void configure(double samplesPerSymbol)
+ {
+ float ratio = (float)samplesPerSymbol / 5.208f;
+ float gain = 5.7f * ratio;
+ mGainInitial = gain;
+ mGain = gain;
+ mGainMax = gain + 0.5f;
+ mGainMin = gain - 0.5f;
+//TODO: figure out a good formula for initial gain settings ..
+// System.out.println("SPS: " + samplesPerSymbol + " Gain - Ratio:" + ratio + " Initial: " + mGainInitial + " Min:" + mGainMin + " Max:" + mGainMax + " Gain" + mGain);
+ }
+
+ /**
+ * Reset after center frequency change or PPM change.
+ */
+ public void reset()
+ {
+ mSynchronized = false;
+ mGain = mGainInitial;
+ }
+
+ /**
+ * Calculates the mean of sync pattern detection for the samples starting at 9 symbol periods before and ending
+ * at the tenth symbol.
+ * @param offset for the sync detection which is just after the sample of the tenth symbol in the sync pattern
+ * @return mean for the samples in this range
+ */
+ public float getBalance(double offset)
+ {
+ Mean mean = new Mean();
+ int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol));
+ int end = (int)Math.ceil(offset);
+
+ for(int i = start; i <= end; i++)
+ {
+ mean.increment(mBuffer[i]);
+ }
+
+ return (float)mean.getResult();
+ }
+
+ /**
+ * On sync detection, calculates the optimal timing adjustment that achieves the highest sync correlation score
+ * or returns an INVALID_SYNC_DETECTION sentinel value if the correlation score doesn't exceed the threshold.
+ *
+ * @param additionalOffset from current bufferPointer and samplePoint to adjust for which sync detector
+ * triggered the event. This can be zero offset for the primary sync detector or a lagging (ie half symbol)
+ * offset for the lagging sync detector.
+ * @return optimized timing adjustment or NO_OPTIMIZATION sentinel value.
+ */
+ public Correction optimize(NXDNSyncDetector syncDetector, double additionalOffset, float detectionScore, double bufferOffset)
+ {
+ //Offset points to the final symbol/sample in the buffer for the detected sync pattern.
+ double offset = bufferOffset + additionalOffset;
+
+ //Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold.
+ double standardDeviation = getStandardDeviation(offset);
+
+ if(standardDeviation > mNoiseStandardDeviationThreshold)
+ {
+ return INVALID_SYNC_DETECTION;
+ }
+
+ float calculatedBalanceCorrection = -getBalance(offset);
+
+ //Find the optimal symbol timing
+ double samplesPerSymbol = mSamplesPerSymbol;
+ double stepSize = samplesPerSymbol / (mSynchronized ? 16.0 : 8.0); //Start at 1/8th for coarse & 1/16th for fine
+ double stepSizeMin = mOptimizeFineIncrement;
+ double adjustment = 0.0;
+
+ //In coarse sync mode, constrain max adjustment to half a symbol period so that a lagging sync detect doesn't
+ //preempt a primary sync detect prematurely and cause a significant timing correction. So, we constrain the
+ //max correction to a half symbol period for coarse sync mode.
+ double adjustmentMax = samplesPerSymbol / 2.0;
+ double candidate = offset;
+
+ float scoreCenter = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+
+ candidate = offset - stepSize;
+ float scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+
+ candidate = offset + stepSize;
+ float scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+
+ while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax)
+ {
+ if(scoreLeft > scoreRight && scoreLeft > scoreCenter)
+ {
+ adjustment -= stepSize;
+ scoreRight = scoreCenter;
+ scoreCenter = scoreLeft;
+
+ candidate = offset + adjustment - stepSize;
+ scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+ }
+ else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
+ {
+ adjustment += stepSize;
+ scoreLeft = scoreCenter;
+ scoreCenter = scoreRight;
+
+ candidate = offset + adjustment + stepSize;
+ scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+ }
+ else
+ {
+ stepSize *= 0.5f;
+
+ if(stepSize > stepSizeMin)
+ {
+ candidate = offset + adjustment - stepSize;
+ scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+
+ candidate = offset + adjustment + stepSize;
+ scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
+ }
+ }
+ }
+
+ //If we didn't find an optimal correlation score above the threshold, return a false sync.
+ if(scoreCenter < SYNC_THRESHOLD_OPTIMIZED)
+ {
+ return INVALID_SYNC_DETECTION;
+ }
+
+ return getCorrection(syncDetector, additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset,
+ calculatedBalanceCorrection);
+ }
+
+ /**
+ * Equalize the symbol by applying balance correction and multiplying with gain value.
+ * @param symbol to equalize.
+ * @return equalized symbol
+ */
+ public float equalize(float symbol)
+ {
+ return symbol * mGain + mBalance;
+ }
+
+ /**
+ * Adjusts the symbol balance from the symbol error to address multi-path and fading between sync periods.
+ */
+ public double getAdjustment(float softSymbol, Dibit symbol, int bufferPointer)
+ {
+ double adjustment;
+
+ if(softSymbol < symbol.getIdealPhase())
+ {
+ if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1])
+ {
+ adjustment = -mSamplePointAdjustmentIncrement;
+ }
+ else
+ {
+ adjustment = mSamplePointAdjustmentIncrement;
+ }
+ }
+ else
+ {
+ if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1])
+ {
+ adjustment = mSamplePointAdjustmentIncrement;
+ }
+ else
+ {
+ adjustment = -mSamplePointAdjustmentIncrement;
+ }
+ }
+
+ if(Math.abs(mSamplePointAdjustment + adjustment) <= mSamplePointAdjustmentMax)
+ {
+ mSamplePointAdjustment += adjustment;
+ return adjustment;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset.
+ *
+ * @param sample1 of the sample stream
+ * @param sample2 of the sample stream
+ * @param mu interpolation point between sample 1 and sample 2
+ * @return equalized symbol
+ */
+ public float getEqualizedSymbol(float sample1, float sample2, double mu)
+ {
+ sample1 = equalize(sample1);
+ sample2 = equalize(sample2);
+ return LinearInterpolator.calculate(sample1, sample2, mu);
+ }
+
+ /**
+ * Gets the interpolated sample value at the specified offset and equalizes it with the current equalizer settings.
+ * @param offset to the sample
+ * @return equalized value.
+ */
+ public float getEqualizedSymbol(double offset)
+ {
+ int integral = (int)Math.floor(offset);
+
+ if(integral >= 0)
+ {
+ return getEqualizedSymbol(mBuffer[integral], mBuffer[integral + 1], offset - integral);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Equalizes both samples using the candidate correction values and returns the interpolated value at the
+ * mu offset.
+ * @param sample1 to equalize
+ * @param sample2 to equalize
+ * @param mu interpolation between samples 1 and 2
+ * @param correction to apply when equalizing samples 1 and 2
+ * @return interpolated value.
+ */
+ public float getEqualizedSymbol(float sample1, float sample2, double mu, Correction correction)
+ {
+ sample1 = (sample1 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment());
+ sample2 = (sample2 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment());
+
+ if(sample1 > Math.PI)
+ {
+ sample1 -= TWO_PI;
+ }
+ else if(sample1 < -Math.PI)
+ {
+ sample1 += TWO_PI;
+ }
+
+ if(sample2 > Math.PI)
+ {
+ sample2 -= TWO_PI;
+ }
+ else if(sample2 < -Math.PI)
+ {
+ sample2 += TWO_PI;
+ }
+
+ return LinearInterpolator.calculate(sample1, sample2, mu);
+ }
+
+ /**
+ * Calculates the sync correlation score at the specified offset and samples per symbol interval using the
+ * current equalizer balance and gain values.
+ *
+ * @param offset to the final symbol in the soft symbol buffer
+ * @param samplesPerSymbol spacing to test for.
+ * @return correlation score.
+ */
+ public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance)
+ {
+ return score(syncDetector, offset, samplesPerSymbol, balance, mGain);
+ }
+
+ /**
+ * Calculates the sync correlation score at the specified offset and samples per symbol interval.
+ * @param offset to the final symbol in the soft symbol buffer
+ * @param samplesPerSymbol spacing to test for.
+ * @return correlation score.
+ */
+ public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance, float gain)
+ {
+ int maxPointer = mBuffer.length - 1;
+ float softSymbol;
+
+ double pointer = offset - (samplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1));
+ int bufferPointer = (int)Math.floor(pointer);
+ double fractional = pointer - bufferPointer;
+
+ float score = 0;
+ float sample1, sample2;
+
+ for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++)
+ {
+ if(bufferPointer < maxPointer)
+ {
+ sample1 = mBuffer[bufferPointer] * gain + balance;
+ sample2 = mBuffer[bufferPointer + 1] * gain + balance;
+ softSymbol = LinearInterpolator.calculate(sample1, sample2, fractional);
+ }
+ else
+ {
+ softSymbol = 0.0f;
+ }
+
+ score += softSymbol * syncDetector.getSyncSymbols()[x];
+ pointer += samplesPerSymbol;
+ bufferPointer = (int)Math.floor(pointer);
+ fractional = pointer - bufferPointer;
+ }
+
+ return score;
+ }
+
+ /**
+ * Applies equalizer correction settings.
+ *
+ * @param correction settings to apply
+ */
+ public void apply(Correction correction)
+ {
+ float remainingBalanceToCorrect = correction.getBalanceAdjustment() - mBalance;
+
+ //Re-initialize the equalizer any time the balance correction value exceeds the threshold.
+ if(mSynchronized && Math.abs(remainingBalanceToCorrect) > EQUALIZER_RECALIBRATE_THRESHOLD)
+ {
+// System.out.println("\t\t ***** RESETTING DUE TO EXCESSIVE GAIN IMBALANCE ******* " + remainingBalanceToCorrect);
+ reset();
+ }
+
+ //Limit equalizer adjustments using a control loop at each sync detect after the initial equalizer setup or
+ //apply as new settings on initial sync detect or any time an excess balance correction is supplied.
+ if(mSynchronized)
+ {
+ mBalance += (remainingBalanceToCorrect * EQUALIZER_LOOP_GAIN);
+ mGain += (correction.getGainAdjustment() * EQUALIZER_LOOP_GAIN);
+ }
+ else
+ {
+ mBalance += remainingBalanceToCorrect;
+ mGain += correction.getGainAdjustment();
+ }
+
+ mBalance = Math.min(mBalance, EQUALIZER_MAXIMUM_BALANCE);
+ mBalance = Math.max(mBalance, -EQUALIZER_MAXIMUM_BALANCE);
+ mGain = Math.min(mGain, mGainMax);
+ mGain = Math.max(mGain, mGainMin);
+ }
+
+ /**
+ * Calculates a correction update for the equalizer balance and gain when the sync pattern is detected in the
+ * sample buffer, using the supplied timing correction argument. Resamples the sync symbols and compares each
+ * soft symbol to the ideal symbol phase to develop average error measurements for balance and gain. On initial
+ * sync detection, the equalizer settings are applied to the samples in the buffer allowing the symbols to be
+ * resampled during coarse sync acquisition.
+ */
+ public Correction getCorrection(NXDNSyncDetector syncDetector, double additionalOffset, double timingCorrection,
+ float detectionScore, float optimizationScore, double offset, float balanceCorrection)
+ {
+ double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1));
+ int resampleStartIntegral = (int)Math.floor(resampleStart);
+ float symbol, gainAccumulator = 0, resampledSoftSymbol;
+ Dibit resampledDibit;
+ int bitErrorCount = 0;
+
+ for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++)
+ {
+ if(resampleStartIntegral >= 0)
+ {
+ symbol = syncDetector.getSyncSymbols()[x];
+ resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
+ mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
+ resampledSoftSymbol = resampledSoftSymbol * mGain + balanceCorrection;
+ resampledDibit = toSymbol(resampledSoftSymbol);
+ bitErrorCount += syncDetector.getSyncDibits()[x].getBitErrorFrom(resampledDibit);
+ gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol);
+ }
+
+ resampleStart += mSamplesPerSymbol;
+ resampleStartIntegral = (int)Math.floor(resampleStart);
+ }
+
+ gainAccumulator /= syncDetector.getSyncPatternDibitLength();
+
+ //Recalculate the optimization score using the adjusted equalizer settings
+ if(!mSynchronized)
+ {
+ double optimizedFinalSymbol = offset + additionalOffset + timingCorrection;
+ balanceCorrection = -getBalance(optimizedFinalSymbol);
+ optimizationScore = score(syncDetector, optimizedFinalSymbol, mSamplesPerSymbol,
+ balanceCorrection, mGain + gainAccumulator);
+ }
+
+ return new Correction(additionalOffset, timingCorrection, balanceCorrection, gainAccumulator, detectionScore,
+ optimizationScore, bitErrorCount);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java
new file mode 100644
index 00000000..1362e55e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java
@@ -0,0 +1,794 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.controller.channel.ChannelEvent;
+import io.github.dsheirer.controller.channel.IChannelEventListener;
+import io.github.dsheirer.controller.channel.IChannelEventProvider;
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.identifier.MutableIdentifierCollection;
+import io.github.dsheirer.identifier.alias.TalkerAliasManager;
+import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
+import io.github.dsheirer.identifier.radio.RadioIdentifier;
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.module.decode.event.IDecodeEvent;
+import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
+import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
+import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
+import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelFake;
+import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkerAliasIdentifier;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.CallTimer;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.CallType;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.DataCallOption;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.Duplex;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.VoiceCallOption;
+import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.config.SourceConfigTuner;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages trunked system traffic channel activations
+ */
+public class NXDNTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider,
+ IChannelEventListener, IChannelEventProvider
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(NXDNTrafficChannelManager.class);
+ public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED";
+ public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED";
+ private final Channel mParentChannel;
+ private final DecodeEventDuplicateDetector mDuplicateDetector = new DecodeEventDuplicateDetector();
+ private final List mChannelFrequencies = new ArrayList<>();
+ private final Lock mLock = new ReentrantLock();
+ private final Map mAllocatedTrafficChannelMap = new HashMap<>();
+ private final Map mEventTrackerMap = new HashMap<>();
+ private final Queue mAvailableTrafficChannelQueue = new LinkedTransferQueue<>();
+ private final TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager();
+ private final TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor();
+ private List mManagedTrafficChannels;
+ private Listener mDecodeEventListener;
+ private Listener mChannelEventListener;
+ private ChannelAccessInformation mChannelAccessInformation;
+ private boolean mIgnoreDataCalls;
+ private boolean mIgnoreEncryptedCalls;
+
+ /**
+ * Constructs an instance
+ *
+ * @param parentChannel configuration for the parent channel
+ */
+ public NXDNTrafficChannelManager(Channel parentChannel)
+ {
+ mParentChannel = parentChannel;
+
+ if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
+ {
+ mIgnoreDataCalls = configNXDN.isIgnoreDataCalls();
+ mIgnoreEncryptedCalls = configNXDN.isIgnoreEncryptedCalls();
+ mChannelFrequencies.addAll(configNXDN.getChannelMap());
+ createTrafficChannels(configNXDN);
+ }
+ }
+
+ /**
+ * Talker alias manager
+ */
+ public TalkerAliasManager getTalkerAliasManager()
+ {
+ return mTalkerAliasManager;
+ }
+
+ /**
+ * Sets the channel access info from the control channel.
+ *
+ * @param info from the control channel
+ */
+ public void setChannelAccessInformation(ChannelAccessInformation info)
+ {
+ if(mChannelAccessInformation == null)
+ {
+ mChannelAccessInformation = info;
+ }
+ }
+
+ /**
+ * Creates up to the maximum number of traffic channels for use in allocating traffic channels.
+ *
+ * @param decodeConfig to use for each traffic channel
+ */
+ private void createTrafficChannels(DecodeConfigNXDN decodeConfig)
+ {
+ if(mManagedTrafficChannels == null)
+ {
+ int trafficChannelPoolSize = decodeConfig.getTrafficChannelPoolSize();
+ List trafficChannelList = new ArrayList<>();
+
+ if(trafficChannelPoolSize > 0)
+ {
+ for(int x = 0; x < trafficChannelPoolSize; x++)
+ {
+ Channel trafficChannel = new Channel("T-" + mParentChannel.getName(), Channel.ChannelType.TRAFFIC);
+ trafficChannel.setAliasListName(mParentChannel.getAliasListName());
+ trafficChannel.setSystem(mParentChannel.getSystem());
+ trafficChannel.setSite(mParentChannel.getSite());
+ trafficChannel.setDecodeConfiguration(decodeConfig);
+ trafficChannel.setEventLogConfiguration(mParentChannel.getEventLogConfiguration());
+ trafficChannel.setRecordConfiguration(mParentChannel.getRecordConfiguration());
+ trafficChannelList.add(trafficChannel);
+ }
+ }
+
+ mAvailableTrafficChannelQueue.addAll(trafficChannelList);
+ mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList);
+ }
+ }
+
+ /**
+ * Retrieves the current event tracker for the specified channel and if the tracker is stale relative to the
+ * timestamp, returns null, otherwise returns the current tracker.
+ *
+ * @param frequency to look up the tracker
+ * @param timestamp to compare for staleness
+ * @return tracker or null
+ */
+ private NXDNChannelEventTracker getTrackerRemoveIfStale(long frequency, long timestamp)
+ {
+ NXDNChannelEventTracker tracker = mEventTrackerMap.get(frequency);
+
+ if(tracker != null && tracker.isStale(timestamp))
+ {
+ removeTracker(frequency);
+ tracker = null;
+ }
+
+ return tracker;
+ }
+
+ /**
+ * Broadcasts the decode event from the tracker.
+ *
+ * @param tracker containing a decode event.
+ */
+ public void broadcast(NXDNChannelEventTracker tracker)
+ {
+ broadcast(tracker.getEvent());
+ }
+
+ /**
+ * Broadcasts an initial or update decode event to any registered listener.
+ */
+ public void broadcast(DecodeEvent decodeEvent)
+ {
+ if(mDecodeEventListener != null)
+ {
+ if(decodeEvent.getEventType() == DecodeEventType.DATA_CALL && mDuplicateDetector.isDuplicate(decodeEvent,
+ System.currentTimeMillis()))
+ {
+ return;
+ }
+
+ mDecodeEventListener.receive(decodeEvent);
+ }
+ else
+ {
+ System.out.println("Decode event listener is null");
+ }
+ }
+
+ /**
+ * Selects the decode event type based on the call type and encryption.
+ *
+ * @param callType for the call
+ * @param encryption for the call
+ * @return decode event type
+ */
+ private DecodeEventType getType(CallType callType, EncryptionKeyIdentifier encryption)
+ {
+ boolean encrypted = encryption.isEncrypted();
+ return switch(callType)
+ {
+ case GROUP_BROADCAST, GROUP_CONFERENCE ->
+ encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP;
+ case INDIVIDUAL ->
+ encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT;
+ case INTERCONNECT, SPEED_DIAL ->
+ encrypted ? DecodeEventType.CALL_INTERCONNECT_ENCRYPTED : DecodeEventType.CALL_INTERCONNECT;
+ default -> encrypted ? DecodeEventType.CALL_ENCRYPTED : DecodeEventType.CALL;
+ };
+ }
+
+ /**
+ * Creates a traffic channel event tracker
+ *
+ * @param eventType for the decode event
+ * @param ic collection of from/to identifiers
+ * @param channel to track
+ * @param timestamp for the event
+ * @return tracker
+ */
+ private NXDNChannelEventTracker createTracker(DecodeEventType eventType, IdentifierCollection ic,
+ NXDNChannel channel, long timestamp)
+ {
+ long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
+
+ DecodeEvent event = NXDNDecodeEvent.builder(eventType, timestamp)
+ .timeslot(0)
+ .channel(channel)
+ .identifiers(ic)
+ .build();
+ NXDNChannelEventTracker tracker = new NXDNChannelEventTracker(event);
+ mEventTrackerMap.put(frequency, tracker);
+ return tracker;
+ }
+
+ /**
+ * Process a data call assignment
+ *
+ * @param dca to process
+ */
+ public void processDataCallAssignment(DataCallAssignment dca)
+ {
+ if(dca.hasChannel() && dca.getChannel().getDownlinkFrequency() > 0)
+ {
+ DecodeEventType eventType = dca.getEncryptionKeyIdentifier().isEncrypted() ?
+ DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL;
+ processDataCall(dca.getIdentifiers(), dca.getChannel(), eventType, dca.getTimestamp(), dca.getCallOption(),
+ dca.getCallTimer());
+ }
+ }
+
+ /**
+ * Process a voice call assignment or notification
+ * @param identifiers for the call
+ * @param channel for the call
+ */
+ private void processDataCall(List identifiers, NXDNChannel channel, DecodeEventType eventType,
+ long timestamp, DataCallOption dco, CallTimer callTimer)
+ {
+ if(channel.getDownlinkFrequency() > 0)
+ {
+ mLock.lock();
+
+ try
+ {
+ NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(channel.getDownlinkFrequency(), timestamp);
+ MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers);
+ if(tracker != null)
+ {
+ if(tracker.isSameCallCheckingToAndFrom(ic, timestamp))
+ {
+ tracker.updateDurationControl(timestamp);
+ broadcast(tracker);
+ }
+ else
+ {
+ removeTracker(channel.getDownlinkFrequency());
+ tracker = null;
+ }
+ }
+
+ if(tracker == null)
+ {
+ tracker = createTracker(eventType, ic, channel, timestamp);
+ Duplex duplex = dco.getDuplex();
+ TransmissionMode mode = dco.getTransmissionMode();
+ tracker.addDetails("TIMER:" + callTimer + " " + duplex + " " + mode);
+ }
+
+ if(mIgnoreDataCalls)
+ {
+ tracker.prefixDetails("IGNORED DATA");
+ }
+ else
+ {
+ if(!tracker.isTrafficChannelAllocated() && channel.isValid() &&
+ channel.getDownlinkFrequency() != getCurrentControlFrequency() &&
+ !mAllocatedTrafficChannelMap.containsKey(channel.getDownlinkFrequency()))
+ {
+ //Retrieve a channel from the traffic channel queue
+ Channel traffic = mAvailableTrafficChannelQueue.poll();
+
+ if(traffic != null)
+ {
+ requestTrafficChannelStart(traffic, channel, ic, tracker);
+ }
+ else
+ {
+ tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails());
+ }
+ }
+ }
+
+ broadcast(tracker);
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Process a voice call assignment indicating there is a call on another channel.
+ *
+ * @param vca to process
+ */
+ public void processVoiceCallAssignment(VoiceCallAssignment vca)
+ {
+ if(vca.hasChannel() && vca.getChannel().getDownlinkFrequency() > 0)
+ {
+ processVoiceCall(vca.getIdentifiers(), vca.getChannel(), vca.getCallType(), vca.getEncryptionKeyIdentifier(),
+ vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer());
+ }
+ }
+
+ /**
+ * Process a voice call assignment duplicate notification from a traffic channel
+ *
+ * @param vca to process
+ */
+ public void processVoiceCallAssignment(VoiceCallAssignmentDuplicateTraffic vca)
+ {
+ NXDNChannel channel = vca.getChannel();
+
+ if(channel == null || channel.getDownlinkFrequency() == 0)
+ {
+ //If the channel is null or not configured, use the talkgroup value so we can track the call event
+ channel = new NXDNChannelFake(vca.getDestination().getValue());
+ }
+
+ processVoiceCall(vca.getIdentifiers(), channel, vca.getCallType(), vca.getEncryptionKeyIdentifier(),
+ vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer());
+ }
+
+ /**
+ * Process a traffic channel voice call message
+ * @param voiceCall message
+ * @param channel for the call, can be null
+ */
+ public void processVoiceCall(VoiceCall voiceCall, IChannelDescriptor channel)
+ {
+ NXDNChannel nxdn = null;
+ if(channel instanceof NXDNChannel nxdnChannel)
+ {
+ nxdn = nxdnChannel;
+ }
+
+ processVoiceCall(voiceCall.getIdentifiers(), nxdn, voiceCall.getCallType(), voiceCall.getEncryptionKeyIdentifier(),
+ voiceCall.getTimestamp(), voiceCall.getCallOption(), CallTimer.UNSPECIFIED);
+ }
+
+ /**
+ * Adds or updates the talker alias for the tracked call event.
+ * @param channel for the call
+ * @param talkerAlias to add or update
+ * @param timestamp for the talker alias message
+ */
+ public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias,
+ RadioIdentifier radio, long timestamp)
+ {
+ mLock.lock();
+
+ try
+ {
+ long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
+ NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
+
+ if(tracker != null)
+ {
+ tracker.addIdentifierIfMissing(talkerAlias);
+ broadcast(tracker);
+ }
+
+ getTalkerAliasManager().update(radio, talkerAlias);
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Updates the end time for an ongoing call to reflect progress
+ * @param channel descriptor for the traffic channel
+ */
+ public void processCallProgressUpdate(IChannelDescriptor channel, long timestamp)
+ {
+ mLock.lock();
+ try
+ {
+ long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
+ NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
+
+ if(tracker != null)
+ {
+ tracker.updateDurationTraffic(timestamp);
+ broadcast(tracker);
+ }
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Ends the current call and removes the event tracker
+ * @param channel for the call
+ * @param timestamp for the end call
+ */
+ public void processEndCall(IChannelDescriptor channel, long timestamp)
+ {
+ mLock.lock();
+
+ try
+ {
+ long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
+ NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
+
+ if(tracker != null)
+ {
+ tracker.completeTraffic(timestamp);
+ broadcast(tracker);
+ removeTracker(frequency);
+ }
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Process a voice call assignment or notification
+ * @param identifiers for the call
+ * @param channel for the call
+ */
+ private void processVoiceCall(List identifiers, NXDNChannel channel, CallType callType,
+ EncryptionKeyIdentifier encryption, long timestamp, VoiceCallOption vco,
+ CallTimer callTimer)
+ {
+ mLock.lock();
+
+ long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
+
+ try
+ {
+ NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
+ MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers);
+ mTalkerAliasManager.enrich(ic);
+ if(tracker != null)
+ {
+ if(tracker.isSameCallCheckingToAndFrom(ic, timestamp))
+ {
+ tracker.updateDurationControl(timestamp);
+ }
+ else
+ {
+ removeTracker(frequency);
+ tracker = null;
+ }
+ }
+
+ if(tracker == null)
+ {
+ DecodeEventType eventType = getType(callType, encryption);
+ tracker = createTracker(eventType, ic, channel, timestamp);
+ AudioCodec audioCodec = vco.getCodec();
+ TransmissionMode mode = vco.getTransmissionMode();
+
+ if(encryption.isEncrypted())
+ {
+ tracker.addDetails(encryption + " TIMER:" + callTimer + " " + audioCodec + " " + mode);
+ }
+ else
+ {
+ tracker.addDetails("TIMER:" + callTimer + " " + audioCodec + " " + mode);
+ }
+ }
+
+ boolean ignoreEncrypted = mIgnoreEncryptedCalls && encryption.isEncrypted();
+
+ if(ignoreEncrypted)
+ {
+ tracker.prefixDetails("IGNORED ENCRYPTED");
+ }
+ else if(!tracker.isTrafficChannelAllocated() && channel != null && channel.isValid() &&
+ channel.getDownlinkFrequency() != getCurrentControlFrequency() &&
+ !mAllocatedTrafficChannelMap.containsKey(frequency))
+ {
+ //Retrieve a channel from the traffic channel queue
+ Channel traffic = mAvailableTrafficChannelQueue.poll();
+
+ if(traffic != null)
+ {
+ requestTrafficChannelStart(traffic, channel, ic, tracker);
+ }
+ else
+ {
+ tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails());
+ }
+ }
+
+ broadcast(tracker);
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Sends a channel start request to the ChannelProcessingManager.
+ * Note: this method is not thread safe and the calling method must protect access using mLock.
+ *
+ * @param trafficChannel to use for the traffic channel
+ * @param nxdnChannel that describes the traffic channel downlink frequency
+ * @param ic containing identifiers for the call
+ * @param tracker to update with channel allocation flag
+ */
+ private void requestTrafficChannelStart(Channel trafficChannel, NXDNChannel nxdnChannel,
+ IdentifierCollection ic, NXDNChannelEventTracker tracker)
+ {
+ if(nxdnChannel != null && nxdnChannel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null)
+ {
+ SourceConfigTuner sourceConfig = new SourceConfigTuner();
+ sourceConfig.setFrequency(nxdnChannel.getDownlinkFrequency());
+ if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
+ {
+ sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
+ }
+ trafficChannel.setSourceConfiguration(sourceConfig);
+ mAllocatedTrafficChannelMap.put(nxdnChannel.getDownlinkFrequency(), trafficChannel);
+ ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel,
+ nxdnChannel, ic, this);
+ startChannelRequest.addPreloadDataContent(new NXDNChannelInfoPreloadData(mChannelAccessInformation, mChannelFrequencies));
+ getInterModuleEventBus().post(startChannelRequest);
+ tracker.setTrafficChannelAllocated(true);
+ }
+ else
+ {
+ //Return the channel to the traffic channel pool if we didn't start it.
+ mAvailableTrafficChannelQueue.add(trafficChannel);
+ }
+ }
+
+ @Override
+ public Listener getChannelEventListener()
+ {
+ return mTrafficChannelTeardownMonitor;
+ }
+
+ /**
+ * Broadcasts the channel event ot an optionally registered listener
+ * @param event to broadcast
+ */
+ private void broadcast(ChannelEvent event)
+ {
+ if(mChannelEventListener != null)
+ {
+ mChannelEventListener.receive(event);
+ }
+ }
+
+ @Override
+ public void setChannelEventListener(Listener listener)
+ {
+ mChannelEventListener = listener;
+ }
+
+ @Override
+ public void removeChannelEventListener()
+ {
+ mChannelEventListener = null;
+ }
+
+ /**
+ * Process the current control frequency to ensure we don't allocated traffic channels for the same frequency.
+ * @param previous frequency for the control channel (to remove from allocated channels)
+ * @param current frequency for the control channel (to add to allocated channels)
+ * @param channel for the current control channel
+ */
+ @Override
+ protected void processControlFrequencyUpdate(long previous, long current, Channel channel)
+ {
+ mLock.lock();
+
+ try
+ {
+ //Clear any traffic channel that is already allocated on the new/current control frequency
+ if(mAllocatedTrafficChannelMap.containsKey(current))
+ {
+ broadcast(new ChannelEvent(mAllocatedTrafficChannelMap.get(current), ChannelEvent.Event.REQUEST_DISABLE));
+ }
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+
+ }
+
+ @Override
+ public void reset()
+ {
+
+ }
+
+ @Override
+ public void start()
+ {
+
+ }
+
+ @Override
+ public void stop()
+ {
+ mLock.lock();
+
+ try
+ {
+ mAvailableTrafficChannelQueue.clear();
+
+ List channels = new ArrayList<>(mAllocatedTrafficChannelMap.values());
+
+ //Issue a disable request for each traffic channel
+ for(Channel channel: channels)
+ {
+ LOGGER.info("Stopping NXDN traffic channel: " + channel);
+ broadcast(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_DISABLE));
+ }
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ @Override
+ public void addDecodeEventListener(Listener listener)
+ {
+ mDecodeEventListener = listener;
+ }
+
+ @Override
+ public void removeDecodeEventListener(Listener listener)
+ {
+ mDecodeEventListener = null;
+ }
+
+ /**
+ * Remotes the event tracker from the tracker map
+ * @param frequency for the tracker
+ */
+ private void removeTracker(long frequency)
+ {
+ mLock.lock();
+
+ try
+ {
+ mEventTrackerMap.remove(frequency);
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Monitors channel teardown events to detect when traffic channel processing has ended or channel start has been
+ * rejected. Reclaims the traffic channel instance for reuse by future traffic channel grants.
+ */
+ public class TrafficChannelTeardownMonitor implements Listener
+ {
+ /**
+ * Process channel events from the ChannelProcessingManager to account for owned child traffic channels.
+ * Note: this method sees events for ALL channels and not just P25 channels managed by this instance.
+ *
+ * @param channelEvent to process
+ */
+ @Override
+ public void receive(ChannelEvent channelEvent)
+ {
+ Channel channel = channelEvent.getChannel();
+
+ if(mManagedTrafficChannels.contains(channel))
+ {
+ mLock.lock();
+
+ try
+ {
+ switch(channelEvent.getEvent())
+ {
+ case NOTIFICATION_PROCESSING_STOP:
+ mAllocatedTrafficChannelMap.entrySet()
+ .stream()
+ .filter(entry -> entry.getValue() == channel)
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .ifPresent(frequency -> {
+ mAllocatedTrafficChannelMap.remove(frequency);
+ //Don't remove the tracker. There's a chance the control channel can still
+ //reference the now terminating traffic channel and cause a residual event
+ //creation and channel allocation and we'll use the tracker to keep that from
+ //happening.
+ //removeTracker(frequency);
+ mAvailableTrafficChannelQueue.add(channel);
+ });
+ break;
+ case NOTIFICATION_PROCESSING_START_REJECTED:
+ mAllocatedTrafficChannelMap.entrySet().stream()
+ .filter(entry -> entry.getValue() == channel)
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .ifPresent(rejectedFrequency -> {
+ mAllocatedTrafficChannelMap.remove(rejectedFrequency);
+ mAvailableTrafficChannelQueue.add(channel);
+
+ //Leave the event in the map so that it doesn't get recreated. The channel
+ //processing manager set the 'tuner not available' in the details already
+ NXDNChannelEventTracker tracker = mEventTrackerMap.get(rejectedFrequency);
+
+ if(tracker != null)
+ {
+ if(!tracker.getEvent().getDetails().contains(CHANNEL_START_REJECTED))
+ {
+ tracker.addDetails(CHANNEL_START_REJECTED + " " + channelEvent.getDescription() +
+ (tracker.getEvent().getDetails() != null ? " - " + tracker.getEvent().getDetails() : ""));
+ }
+ tracker.setTrafficChannelAllocated(false);
+ broadcast(tracker);
+ }
+ });
+ break;
+ }
+ }
+ finally
+ {
+ mLock.unlock();
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java
new file mode 100644
index 00000000..4149940f
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/SyncTracker.java
@@ -0,0 +1,79 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.nxdn;
+
+/**
+ * Tracks sync detections for fixed length messages
+ */
+public class SyncTracker
+{
+ private final int mSymbolsPerMessage;
+ private final int mSyncLossThreshold;
+ private final int mMaxSyncWindowSize;
+ private int mSyncCount = 0;
+ private int mSymbolCount;
+
+ /**
+ * Constructs an instance
+ * @param symbolsPerMessage a count of how many symbols per message, between each sync interval (ie message length).
+ * @param messageWindowSize window size for retaining sync across messages sequences
+ */
+ public SyncTracker(int symbolsPerMessage, int messageWindowSize)
+ {
+ mSymbolsPerMessage = symbolsPerMessage;
+ mSyncLossThreshold = symbolsPerMessage * 2;
+ mMaxSyncWindowSize = messageWindowSize;
+ }
+
+ /**
+ * Increments the symbol count.
+ */
+ public void symbol()
+ {
+ mSymbolCount++;
+
+ if(mSymbolCount >= mSyncLossThreshold)
+ {
+ mSymbolCount -= mSymbolsPerMessage;
+ mSyncCount--;
+ mSyncCount = Math.max(mSyncCount, 0);
+ }
+ }
+
+ /**
+ * Indicates when a sync pattern is detected.
+ */
+ public void syncDetected()
+ {
+ mSyncCount++;
+ mSyncCount = Math.min(mSyncCount, mMaxSyncWindowSize);
+ mSymbolCount -= mSymbolsPerMessage;
+ mSymbolCount = Math.max(mSymbolCount, 0);
+ }
+
+ /**
+ * Indicates if there has been at least one sync detection in the past maxSyncSequence message windows.
+ * @return true if there is at least one sync detection
+ */
+ public boolean isSynchronized()
+ {
+ return mSyncCount > 0;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java
new file mode 100644
index 00000000..ae61a9d1
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/audio/NXDNAudioModule.java
@@ -0,0 +1,175 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2026 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+package io.github.dsheirer.module.decode.nxdn.audio;
+
+import io.github.dsheirer.alias.AliasList;
+import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
+import io.github.dsheirer.audio.squelch.SquelchState;
+import io.github.dsheirer.audio.squelch.SquelchStateEvent;
+import io.github.dsheirer.dsp.gain.NonClippingGain;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.TransmissionRelease;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
+import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallWithOptionalLocation;
+import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
+import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.sample.Listener;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * NXDN AMBE audio module
+ */
+public class NXDNAudioModule extends AmbeAudioModule
+{
+ private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
+ private final NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f);
+ private final List