#7 NXDN Decoder WIP

This commit is contained in:
sheirerd
2025-11-20 07:21:45 -05:00
committed by denny
parent a053315675
commit a79ab1c607
444 changed files with 31047 additions and 2075 deletions

45
.idea/misc.xml generated
View File

@@ -4,7 +4,50 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25_PREVIEW" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
<option name="myOrdered" value="true" />
<option name="myNullables">
<value>
<list size="13">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="8" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="13">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="8" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25_PREVIEW" project-jdk-name="zulu-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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());
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
@@ -50,6 +47,8 @@ public enum TalkgroupFormat
"<html>MPT-1327 valid ranges are 0-127(prefix)<br>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",
"<html>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:

View File

@@ -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());
}

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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;

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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
*

View File

@@ -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;
}
/**

View File

@@ -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()
{

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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;
*/

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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<IdentifierUpdateNotification>
public static class IdentifierUpdateNotificationProxy implements Listener<IdentifierUpdateNotification>
{
private Listener<IdentifierUpdateNotification> mIdentifierUpdateNotificationListener;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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)
{

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
private INativeBufferProvider mNativeBufferProvider;
private TunerController mTunerController;
private List<PolyphaseChannelSource> 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 [" +

View File

@@ -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<Integer> 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<SourceEvent> producerSourceEventListener, String threadName)
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator,
SynthesisFilterManager filterManager,
Listener<SourceEvent> 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

View File

@@ -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)
{

View File

@@ -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
*/

View File

@@ -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++)
{

View File

@@ -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<Dibit>, 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<Dibit>, 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<Dibit>, 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.
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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");
}

View File

@@ -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<T> 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");

View File

@@ -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<ProcessingC
{
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelSpectrumPanel.class);
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private static final DecimalFormat PPM_FORMAT = new DecimalFormat("0.00000");
private static final String LABEL_PREFIX_PPM = "Tuner PPM: ";
private static final String LABEL_PREFIX_TUNER = "Tuner:";
private static final String LABEL_PREFIX_CHANNEL = "Channel:";
private static final String LABEL_PREFIX_DECODER = "Decoder:";
private final PlaylistManager mPlaylistManager;
private ProcessingChain mProcessingChain;
private final ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
@@ -83,7 +90,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
private final FrequencyOverlayPanel mFrequencyOverlayPanel;
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private final SpinnerNumberModel mNoiseFloorSpinnerModel;
private final JLabel mEstimatedCarrierOffsetFrequencyValueLabel;
private final JLabel mTunerPPMLabel;
private final JLabel mTunerCorrectionLabel;
private final JLabel mChannelCorrectionLabel;
private final JLabel mDecoderCorrectionLabel;
private final JLabel mDummyLabel = new JLabel(" ");
private boolean mPanelVisible = false;
private boolean mDftProcessing = false;
private final NoiseSquelchView mNoiseSquelchView;
@@ -92,6 +103,9 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
private final JFXPanel mNoiseSquelchPanel;
private final JFXPanel mSymbolPanel;
private JSplitPane mSplitPane;
private JButton mAButton;
private JButton mBButton;
private FeedbackDecoder mFeedbackDecoder;
/**
* Constructs an instance.
@@ -107,12 +121,51 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
fftPanel.setLayout(new MigLayout("insets 0", "[grow,fill]", "[][grow,fill]"));
JPanel labelPanel = new JPanel();
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][grow,fill,left][right][][]", ""));
labelPanel.add(new JLabel("Channel Spectrum "));
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][][][][][grow,fill][][]", ""));
mEstimatedCarrierOffsetFrequencyValueLabel = new JLabel(getPaddedCarrierOffsetLabel(0));
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
labelPanel.add(mEstimatedCarrierOffsetFrequencyValueLabel);
mTunerPPMLabel = new JLabel(LABEL_PREFIX_PPM);
mTunerPPMLabel.setEnabled(false);
labelPanel.add(mTunerPPMLabel);
labelPanel.add(new JLabel("Corrections (Hz) "));
mTunerCorrectionLabel = new JLabel(LABEL_PREFIX_TUNER);
mTunerCorrectionLabel.setEnabled(false);
labelPanel.add(mTunerCorrectionLabel);
mChannelCorrectionLabel = new JLabel(LABEL_PREFIX_CHANNEL);
mChannelCorrectionLabel.setEnabled(false);
labelPanel.add(mChannelCorrectionLabel);
mDecoderCorrectionLabel = new JLabel(LABEL_PREFIX_DECODER);
mDecoderCorrectionLabel.setEnabled(false);
labelPanel.add(mDecoderCorrectionLabel);
labelPanel.add(mDummyLabel);
mAButton = new JButton("EQ");
mAButton.setToolTipText("Display the current equalizer tap values");
mAButton.addActionListener(e -> {
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<ProcessingC
mSpectrumPanel.setSampleSize(number.doubleValue());
});
JSpinner noiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
labelPanel.add(new JLabel("Noise Floor:"));
labelPanel.add(noiseFloorSpinner);
labelPanel.add(new JLabel("Noise Floor"));
JButton logIndexesButton = new JButton("Log Settings");
logIndexesButton.addActionListener(e -> {
@@ -229,11 +282,15 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
mSpectrumPanel.clearSpectrum();
}
private String getPaddedCarrierOffsetLabel(long value)
/**
* Pads the value to a fixed width and returns the previx and padded value.
* @param prefix of the label
* @param value to pad with spaces
* @return padded, prefixed label
*/
private String getPaddedLabel(String prefix, Number value)
{
String paddedValue = StringUtils.leftPad(String.valueOf(value), 5);
String paddedText = StringUtils.rightPad(paddedValue + " Hz", 20);
return "Carrier Offset: " + paddedText;
return prefix + StringUtils.leftPad(String.valueOf(value), 7);
}
/**
@@ -297,22 +354,6 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
}
}
/**
* Updates the CarrierOffsetProcessor's current carrier offset tracking frequency
* @param carrierOffsetFrequency that is currently measured/estimated.
*/
private void updateEstimatedCarrierOffsetFrequency(long carrierOffsetFrequency)
{
//Note: we flip the sign on the error measurement because the value represents the amount of offset the PLL
//has to apply to move the signal to center/baseband
EventQueue.invokeLater(() -> {
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<ProcessingC
*/
private void reset()
{
mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(0));
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
mTunerPPMLabel.setText(LABEL_PREFIX_PPM);
mTunerPPMLabel.setEnabled(false);
mTunerCorrectionLabel.setText(LABEL_PREFIX_TUNER);
mTunerCorrectionLabel.setEnabled(false);
mChannelCorrectionLabel.setText(LABEL_PREFIX_CHANNEL);
mChannelCorrectionLabel.setEnabled(false);
mDecoderCorrectionLabel.setText(LABEL_PREFIX_DECODER);
mDecoderCorrectionLabel.setEnabled(false);
mFrequencyOverlayPanel.process(SourceEvent.frequencyChange(null, 0));
mFrequencyOverlayPanel.process(SourceEvent.sampleRateChange(0));
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(0);
@@ -350,6 +397,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
mSymbolView.setProtocol("");
mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
mProcessingChain.removeModule(mSampleStreamTapModule);
mFeedbackDecoder = null;
}
//Invoking reset - we're on the Swing dispatch thread here
@@ -377,6 +425,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
setRightComponent(mSymbolPanel);
mSymbolView.setSymbolProvider(feedbackDecoder);
mSymbolView.setProtocol(feedbackDecoder.getProtocolDescription());
mFeedbackDecoder = feedbackDecoder;
}
mProcessingChain.addModule(mSampleStreamTapModule);
@@ -428,11 +477,38 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED)
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS)
{
updateEstimatedCarrierOffsetFrequency(sourceEvent.getValue().longValue());
if(sourceEvent instanceof ChannelFrequencyCorrectionStatusNotification status)
{
//Note: we flip the sign on the error measurement because the value represents the amount of offset the PLL
//has to apply to move the signal to center/baseband
EventQueue.invokeLater(() -> {
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);
}
}

View File

@@ -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<Alias>
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<Alias>
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<Alias>
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;

View File

@@ -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<Radio>
"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),

View File

@@ -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<RadioRange>
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),

View File

@@ -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<Talkgroup>
"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),

View File

@@ -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<TalkgroupRange>
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<TalkgroupRange>
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<TalkgroupRange>
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;
}

View File

@@ -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:

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Integer> mTrafficChannelPoolSizeSpinner;
private TableView<ObservableChannelFrequency> mChannelMapTable;
private IntegerTextField mChannelField;
private FrequencyField mDownlinkFrequencyField;
private Button mAddTimeslotFrequencyButton;
private Button mDeleteButton;
private Spinner<Integer> mChannelRotationDelaySpinner;
private SegmentedButton mTransmissionModeButton;
private ComboBox<Encoding> 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<EventLogType> 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<Encoding> 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<ObservableChannelFrequency> getChannelMapTable()
{
if(mChannelMapTable == null)
{
mChannelMapTable = new TableView<>(FXCollections.observableArrayList(ObservableChannelFrequency.extractor()));
mChannelMapTable.setPrefHeight(100.0);
TableColumn<ObservableChannelFrequency,Number> channelColumn = new TableColumn<>("Channel");
channelColumn.setPrefWidth(100);
channelColumn.setCellValueFactory(cellData -> cellData.getValue().channelProperty());
mChannelMapTable.getColumns().add(channelColumn);
mChannelMapTable.getSortOrder().add(channelColumn);
TableColumn<ObservableChannelFrequency,Number> 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<String>()
{
@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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<RecorderType> 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<ChannelFrequency> 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);
}
}

View File

@@ -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),

View File

@@ -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<ChannelFrequency> getChannelMap(SystemInformation systemInformation, Site site)
{
if(hasChannelMap(systemInformation, site))
{
List<ChannelFrequency> 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":

View File

@@ -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<ChannelFrequency> 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())

View File

@@ -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();

View File

@@ -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)

View File

@@ -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<DecodeEventSnapshot> 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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -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()));
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<MessagePackage> mMessagePackageTableView;
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
private FilteredList<MessagePackage> 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<MessagePackage> messagePackages = new ArrayList<>();
NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config);
Broadcaster<IMessage> 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<DecoderStateEvent> 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<MessagePackage> 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<MessagePackage> getMessagePackageTableView()
{
if(mMessagePackageTableView == null)
{
mMessagePackageTableView = new TableView<>();
mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
mMessagePackageTableView.setItems(sortedList);
mMessagePackageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> 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<TableColumn.CellDataFeatures, ObservableValue>) 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<IMessage>
{
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<File> 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();
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Number, Number> mSymbolSeries = new XYChart.Series<>();
private final XYChart.Series<Number, Number> mSymbolSeriesMarker = new XYChart.Series<>();
private final ScatterChart<Number, Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
private final XYChart.Series<Number, Number> mConstellationSymbols = new XYChart.Series<>();
private static final int CONSTELLATION_SYMBOL_COUNT = 50;
private int mConstellationSymbolPointer = 0;
private final LineChart<Number, Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
private final ScatterChart<Number, Number> 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<Number, Number> 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<Number, Number> 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<Number, Number> data : mSymbolSeriesMarker.getData())
{
data.setXValue(mSymbolPointer);
}
mSymbolSeries.getData().get(mSymbolPointer++).setYValue(symbol);
mSymbolPointer %= SYMBOL_CHART_SERIES_COUNT;
XYChart.Data<Number,Number> 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<Number, Number> 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<Number, Number> 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<Number, Number> balanceSeries = new XYChart.Series<>();
balanceSeries.setName("Balance");
XYChart.Series<Number, Number> 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);
});
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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,

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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<IChannelDescriptor>
public class ChannelDescriptorConfigurationIdentifier extends Identifier<IBandChannelDescriptor>
{
/**
* Constructs a channel descriptor configuration identifier
*/
public ChannelDescriptorConfigurationIdentifier(IChannelDescriptor value)
public ChannelDescriptorConfigurationIdentifier(IBandChannelDescriptor value)
{
super(value, IdentifierClass.CONFIGURATION, Form.CHANNEL_DESCRIPTOR, Role.ANY);
}

View File

@@ -30,7 +30,9 @@ import io.github.dsheirer.protocol.Protocol;
public class EncryptionKeyIdentifier extends Identifier<EncryptionKey>
{
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<EncryptionKey>
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
return mProtocol;
}
public boolean isEncrypted()
@@ -46,8 +48,8 @@ public class EncryptionKeyIdentifier extends Identifier<EncryptionKey>
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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.
* <p>
* 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;
}
}

View File

@@ -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<Module> 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<State> 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();

View File

@@ -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<DecoderType> 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<DecoderType> 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<DecoderType> AUX_DECODERS =
EnumSet.of(DecoderType.DCS,
DecoderType.FLEETSYNC2,
DecoderType.LJ_1200,
DecoderType.MDC1200,
DecoderType.TAIT_1200);
public static final EnumSet<DecoderType> AUX_DECODERS = EnumSet.of(DCS, FLEETSYNC2, LJ_1200, MDC1200, TAIT_1200);
/**
* Decoders that produce a (recordable) bitstream
*/
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR,
DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DMR, MPT1327, NXDN, P25_PHASE1, P25_PHASE2);
/**
* Decoders that produce (recordable) MBE audio codec frames
*/
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS =
EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS = EnumSet.of(DMR, NXDN, P25_PHASE1, P25_PHASE2);
public Protocol getProtocol()
{

View File

@@ -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<SourceEvent> mSourceEventListener;
private Listener<Float> 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.
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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<SourceEvent> mSourceEventListener = new SourceEventListener();
private DecodeEvent mDecodeEvent;
private IChannelDescriptor mChannelDescriptor = null;
private IBandChannelDescriptor mChannelDescriptor = null;
public AnalogDecoderState()
{

View File

@@ -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();
}
}

View File

@@ -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<ComplexSamples>
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
ISourceEventListener, Listener<ComplexSamples>
{
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<SourceEvent> 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;
}
}
}

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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();

View File

@@ -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);
}
/**

View File

@@ -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;

View File

@@ -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 <T> type of decode event being tracked.
*/
public class P25TrafficChannelEventTracker
public abstract class ChannelEventTracker<T extends DecodeEvent>
{
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)

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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<ProcessingChain>
{
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<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
private TableRowSorter<TableModel> mTableRowSorter;
private HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
private final IconModel mIconModel;
private final AliasModel mAliasModel;
private final UserPreferences mUserPreferences;
private final TimestampCellRenderer mTimestampCellRenderer;
private final FilterSet<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
private final HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
/**
@@ -93,19 +93,19 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
mUserPreferences = userPreferences;
mTimestampCellRenderer = new TimestampCellRenderer();
mTable = new JTable(mEventModel);
mTableRowSorter = new TableRowSorter<>(mEventModel);
mTableRowSorter.setRowFilter(new EventRowFilter());
mTable.setRowSorter(mTableRowSorter);
mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
TableRowSorter<TableModel> 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<ProcessingChain
{
if(preferenceType == PreferenceType.DECODE_EVENT || preferenceType == PreferenceType.TALKGROUP_FORMAT)
{
EventQueue.invokeLater(() -> mTimestampCellRenderer.updatePreferences());
EventQueue.invokeLater(mTimestampCellRenderer::updatePreferences);
}
}
@@ -215,7 +215,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
for(Identifier identifier: identifiers)
{
if(sb.length() > 0)
if(!sb.isEmpty())
{
sb.append(",");
}
@@ -262,9 +262,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
ImageIcon icon = null;
String text = null;
if(value instanceof IdentifierCollection)
if(value instanceof IdentifierCollection identifierCollection)
{
IdentifierCollection identifierCollection = (IdentifierCollection)value;
List<Identifier> identifiers = identifierCollection.getIdentifiers(mRole);
if(identifiers != null && !identifiers.isEmpty())
@@ -281,13 +280,13 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
if(!aliases.isEmpty())
{
if(sb.length() > 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<ProcessingChain
}
}
public class DurationCellRenderer extends DefaultTableCellRenderer
public static class DurationCellRenderer extends DefaultTableCellRenderer
{
private DecimalFormat mDecimalFormat = new DecimalFormat("0.0");
private final DecimalFormat mDecimalFormat = new DecimalFormat("0.0");
public DurationCellRenderer()
{
@@ -372,9 +371,9 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
/**
* Frequency value cell renderer
*/
public class FrequencyCellRenderer extends DefaultTableCellRenderer
public static class FrequencyCellRenderer extends DefaultTableCellRenderer
{
private DecimalFormat mFrequencyFormatter = new DecimalFormat("0.00000");
private final DecimalFormat mFrequencyFormatter = new DecimalFormat("0.00000");
public FrequencyCellRenderer()
{
@@ -388,10 +387,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
String formatted = null;
if(value instanceof IChannelDescriptor)
if(value instanceof IChannelDescriptor channelDescriptor)
{
IChannelDescriptor channelDescriptor = (IChannelDescriptor)value;
long frequency = channelDescriptor.getDownlinkFrequency();
if(frequency > 0)
@@ -409,7 +406,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
/**
* Channel descriptor value cell renderer
*/
public class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
public static class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
{
public ChannelDescriptorCellRenderer()
{

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.ltrnet;
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.DecoderStateEvent.Event;
@@ -97,7 +97,7 @@ public class LTRNetDecoderState extends DecoderState
}
@Override
protected IChannelDescriptor getCurrentChannel()
protected IBandChannelDescriptor getCurrentChannel()
{
if(mCurrentChannelNumber > 0)
{

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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<LtrNetChannel>
public class LtrNetChannel implements IBandChannelDescriptor, Comparable<LtrNetChannel>
{
private long mDownlink;
private long mUplink;

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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<LtrChannel>
public class LtrChannel implements IBandChannelDescriptor, Comparable<LtrChannel>
{
private long mDownlink;
private int mChannel;

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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;

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ChannelFrequency> 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<ChannelFrequency> getChannelMap()
{
return mChannelMap;
}
/**
* Sets the optional user-provided channel to frequency mapping
* @param channelMap with entries to configure channels
*/
public void setChannelMap(List<ChannelFrequency> 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;
};
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<DecodeEvent>
{
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ChannelInformation>
{
/**
* 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<ChannelFrequency> frequencies)
{
super(new ChannelInformation(info, frequencies));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ComplexSamples>
{
private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class);
private static final Map<String,float[]> 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<ByteBuffer> listener)
{
mSymbolProcessor.setBufferListener(listener);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void removeBufferListener(Listener<ByteBuffer> listener)
{
mSymbolProcessor.setBufferListener(null);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public boolean hasBufferListeners()
{
return mSymbolProcessor.hasBufferListener();
}
@Override
public Listener<SourceEvent> 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<ComplexSamples> getComplexSamplesListener()
{
return this;
}
public static class MyListener implements Listener<IMessage>
{
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<ComplexSamples> 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");
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<Identifier> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<IMessage> 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<IMessage> 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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<IMessage>
{
private final static Logger LOGGER = LoggerFactory.getLogger(NXDNMessageProcessor.class);
private ChannelAccessInformation mChannelAccessInformation;
private final Map<Integer, ChannelFrequency> 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<IMessage> 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<NXDNMessage> 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<IMessage> listener)
{
mMessageListener = listener;
}
/**
* Clears a registered message listener.
*/
public void removeMessageListener()
{
mMessageListener = null;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Integer, Neighbor> mNeighborMap = new HashMap<>();
private AdjacentSiteInformationTypeD mTypeDNeighborA;
private AdjacentSiteInformationTypeD mTypeDNeighborB;
private Integer mTypeDRepeater;
private List<Integer> 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<Integer> 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:
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ChannelFrequency> mChannelFrequencies = new ArrayList<>();
private final Lock mLock = new ReentrantLock();
private final Map<Long, Channel> mAllocatedTrafficChannelMap = new HashMap<>();
private final Map<Long, NXDNChannelEventTracker> mEventTrackerMap = new HashMap<>();
private final Queue<Channel> mAvailableTrafficChannelQueue = new LinkedTransferQueue<>();
private final TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager();
private final TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor();
private List<Channel> mManagedTrafficChannels;
private Listener<IDecodeEvent> mDecodeEventListener;
private Listener<ChannelEvent> 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<Channel> 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<Identifier> 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<Identifier> 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<ChannelEvent> 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<ChannelEvent> 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<Channel> 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<IDecodeEvent> listener)
{
mDecodeEventListener = listener;
}
@Override
public void removeDecodeEventListener(Listener<IDecodeEvent> 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<ChannelEvent>
{
/**
* 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();
}
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Audio> mCachedAudioMessages = new ArrayList<>();
private boolean mEncryptedCall = false;
private boolean mEncryptedCallStateEstablished = false;
private AudioCodec mAudioCodec;
/**
* Constructs an instance
* @param userPreferences component
* @param aliasList for the current channel
*/
public NXDNAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
super(userPreferences, aliasList, 0);
}
@Override
public Listener<SquelchStateEvent> getSquelchStateListener()
{
return mSquelchStateListener;
}
@Override
public void reset()
{
getIdentifierCollection().clear();
}
@Override
public void start()
{
}
/**
* Processes audio and layer 3 messages to decode audio and to determine the encrypted status of a call event.
*/
public void receive(IMessage message)
{
if(hasAudioCodec())
{
if(message instanceof Audio audio)
{
if(mEncryptedCallStateEstablished)
{
processAudio(audio);
}
else
{
//Cache audio until we can determine the encryption state
mCachedAudioMessages.add(audio);
}
}
else if(message.isValid())
{
if(message instanceof VoiceCall voiceCall)
{
mEncryptedCall = voiceCall.getEncryptionKeyIdentifier().isEncrypted();
mEncryptedCallStateEstablished = true;
mAudioCodec = voiceCall.getCallOption().getCodec();
processCachedAudio();
}
else if(message instanceof VoiceCallWithOptionalLocation voiceCall)
{
mEncryptedCall = voiceCall.getEncryptionKeyIdentifier().isEncrypted();
mEncryptedCallStateEstablished = true;
mAudioCodec = voiceCall.getCallOption().getCodec();
processCachedAudio();
}
else if(message instanceof Disconnect || message instanceof TransmissionRelease)
{
closeAudioSegment();
mCachedAudioMessages.clear();
mEncryptedCall = false;
mEncryptedCallStateEstablished = false;
}
}
}
}
/**
* Processes any cached audio frames that were pending an encryption state determination.
*/
private void processCachedAudio()
{
for(Audio audio : mCachedAudioMessages)
{
processAudio(audio);
}
mCachedAudioMessages.clear();
}
/**
* Processes an audio packet by decoding the IMBE audio frames and rebroadcasting them as PCM audio packets.
*/
private void processAudio(Audio audio)
{
if(!mEncryptedCall && mAudioCodec != null && mAudioCodec.equals(AudioCodec.HALF_RATE)) //Full rate not yet supported
{
for(byte[] frame : audio.getAudioFrames())
{
float[] generatedAudio = getAudioCodec().getAudio(frame);
generatedAudio = mGain.apply(generatedAudio);
addAudio(generatedAudio);
}
}
else
{
//Encrypted audio processing not supported
}
}
/**
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
* END audio packet so that downstream processors like the audio recorder can properly close out a call sequence.
*/
public class SquelchStateListener implements Listener<SquelchStateEvent>
{
@Override
public void receive(SquelchStateEvent event)
{
if(event.getSquelchState() == SquelchState.SQUELCH)
{
closeAudioSegment();
mEncryptedCallStateEstablished = false;
mEncryptedCall = false;
mCachedAudioMessages.clear();
}
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.audio;
import io.github.dsheirer.audio.codec.mbe.MBECallSequence;
import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
import io.github.dsheirer.audio.codec.mbe.VoiceFrame;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallInitializationVector;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo2;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo4;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressSourceID;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInfo;
import io.github.dsheirer.preference.UserPreferences;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* NXDN AMBE+ Frame recorder generates call sequence recordings containing JSON representations of audio
* frames, optional encryption and call identifiers.
*/
public class NXDNCallSequenceRecorder extends MBECallSequenceRecorder
{
private final static Logger mLog = LoggerFactory.getLogger(NXDNCallSequenceRecorder.class);
public static final String PROTOCOL = "NXDN";
private MBECallSequence mCallSequence;
/**
* Constructs an instance
*
* @param userPreferences to obtain the recording directory
* @param channelFrequency for the channel to record
* @param system defined by the user
* @param site defined by the user
*/
public NXDNCallSequenceRecorder(UserPreferences userPreferences, long channelFrequency, String system, String site)
{
super(userPreferences, channelFrequency, system, site);
}
/**
* Stops and flushes any partial frame sequence from the processors
*/
@Override
public void stop()
{
flush();
}
/**
* Primary message interface for receiving frames and metadata messages to record
*/
@Override
public void receive(IMessage message)
{
if(message instanceof NXDNMessage nxdn)
{
if(nxdn.isValid())
{
if(nxdn instanceof Audio audio)
{
process(audio);
}
else if(nxdn instanceof NXDNLayer3Message layer3)
{
process(layer3);
}
}
}
}
/**
* Gets and optionally creates a new call sequence.
*/
private MBECallSequence getCallSequence()
{
if(mCallSequence == null)
{
mCallSequence = new MBECallSequence(PROTOCOL);
}
return mCallSequence;
}
/**
* Flushes any partial call sequence
*/
public void flush()
{
if(mCallSequence != null)
{
writeCallSequence(mCallSequence);
mCallSequence = null;
}
}
/**
* Processes any NXDN layer 3 message
*/
public void process(NXDNLayer3Message layer3)
{
switch(layer3.getMessageType())
{
case TRAFFIC_IN_01_CC_VOICE_CALL:
case TRAFFIC_OUT_01_CC_VOICE_CALL:
case TYPE_D_IN_01_CC_VOICE_CALL:
case TYPE_D_OUT_01_CC_VOICE_CALL:
if(layer3 instanceof VoiceCall vc)
{
getCallSequence().setCallType(vc.getCallType().toString());
getCallSequence().setFromIdentifier(vc.getSource());
getCallSequence().setToIdentifier(vc.getDestination());
if(vc.getEncryptionKeyIdentifier().isEncrypted())
{
getCallSequence().setEncrypted(true);
}
}
break;
case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
if(layer3 instanceof VoiceCallInitializationVector iv)
{
//TODO: implement superframes with IV injected into each
}
break;
case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE:
case TYPE_D_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE:
writeCallSequence(mCallSequence);
mCallSequence = null;
break;
case TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_4_CALL_IN_PROGRESS_DESTINATION:
if(layer3 instanceof CallInProgressDestinationInfo4 info4)
{
getCallSequence().setToIdentifier(info4.getDestination());
}
break;
case TYPE_D_SCCH_IN_INFO_3_CALL_IN_PROGRESS_SOURCE:
case TYPE_D_SCCH_OUT_INFO_3_CALL_IN_PROGRESS_SOURCE:
if(layer3 instanceof CallInProgressSourceID source)
{
getCallSequence().setFromIdentifier(source.getSource());
}
break;
case TYPE_D_SCCH_IN_INFO_2_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_2_CALL_IN_PROGRESS_DESTINATION:
if(layer3 instanceof CallInProgressDestinationInfo2 info2)
{
getCallSequence().setToIdentifier(info2.getDestination());
}
break;
case TYPE_D_SCCH_IN_INFO_1_CALL_INFO:
case TYPE_D_SCCH_OUT_INFO_1_CALL_INFO:
if(layer3 instanceof CallInfo ci && ci.getEncryptionKey().isEncrypted())
{
getCallSequence().setEncrypted(true);
}
break;
}
}
/**
* Process audio messages
*/
private void process(Audio audio)
{
List<byte[]> voiceFrames = audio.getAudioFrames();
long timestamp = audio.getTimestamp();
for(int frame = 0; frame < voiceFrames.size(); frame++)
{
if(frame == 0)
{
BinaryMessage frameBits = BinaryMessage.from(voiceFrames.get(frame));
VoiceFrame voiceFrame = new VoiceFrame(timestamp, frameBits.toHexString());
if(audio.hasSACCHFragment())
{
voiceFrame.setTag(audio.getSACCHFragment().getStructure().toString());
}
getCallSequence().add(voiceFrame);
//Voice frames are 20 milliseconds each, so we increment the timestamp by 20 for each one
timestamp += 20;
}
else
{
BinaryMessage frameBits = BinaryMessage.from(voiceFrames.get(frame));
getCallSequence().addVoiceFrame(timestamp, frameBits.toHexString());
//Voice frames are 20 milliseconds each, so we increment the timestamp by 20 for each one
timestamp += 20;
}
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
/**
* NXDN Channel to Frequency mapping.
*/
public class ChannelFrequency
{
private int mChannel;
private long mDownlink;
private long mUplink;
/**
* Constructs an instance
* @param number for the channel (1 - 2048)
* @param down link frequency in Hertz
* @param up link frequency in Hertz
*/
public ChannelFrequency(int number, long down, long up)
{
mChannel = number;
mDownlink = down;
mUplink = up;
}
/**
* Empty constructor for Jackson deserialization
*/
public ChannelFrequency()
{
//Empty Jackson constructor
}
/**
* Channel number
* @return channel number 1-2048
*/
@JacksonXmlProperty(isAttribute = true, localName = "channel")
public int getChannel()
{
return mChannel;
}
/**
* Sets the channel number
* @param channel in range 1 to 2048
*/
public void setChannel(int channel)
{
mChannel = channel;
}
/**
* Downlink frequency
* @return frequency in Hertz
*/
@JacksonXmlProperty(isAttribute = true, localName = "downlink")
public long getDownlink()
{
return mDownlink;
}
/**
* Sets the downlink frequency
* @param downlink frequency in Hertz
*/
public void setDownlink(long downlink)
{
mDownlink = downlink;
}
/**
* Uplink frequency
* @return frequency in Hertz
*/
@JacksonXmlProperty(isAttribute = true, localName = "uplink")
public long getUplink()
{
return mUplink;
}
/**
* Sets the uplink frequency
* @param uplink frequency in Hertz
*/
public void setUplink(long uplink)
{
mUplink = uplink;
}
}

View File

@@ -0,0 +1,61 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.List;
/**
* Channel access and channel map information
*/
public class ChannelInformation
{
private final ChannelAccessInformation mChannelAccessInformation;
private final List<ChannelFrequency> mChannelFrequencies;
/**
* Constructs an instance
* @param access info
* @param map of optional channels/frequencies
*/
public ChannelInformation(ChannelAccessInformation access, List<ChannelFrequency> channels)
{
mChannelAccessInformation = access;
mChannelFrequencies = channels;
}
/**
* Channel access info from the control channel
* @return access info
*/
public ChannelAccessInformation getChannelAccessInformation()
{
return mChannelAccessInformation;
}
/**
* Channel frequencies
* @return list
*/
public List<ChannelFrequency> getChannelFrequencies()
{
return mChannelFrequencies;
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver;
import io.github.dsheirer.protocol.Protocol;
/**
* Base NXDN channel
*/
public abstract class NXDNChannel implements IChannelDescriptor, IChannelInformationReceiver
{
/**
* Constructs an instance
*/
public NXDNChannel()
{
}
/**
* Indicates if this channel has a non-zero downlink frequency.
*/
public boolean isValid()
{
return getDownlinkFrequency() > 0;
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
@Override
public int getTimeslotCount()
{
return 1;
}
@Override
public boolean isTDMAChannel()
{
return false;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("DN:").append(getDownlinkFrequency() / 1E6D);
long up = getUplinkFrequency();
if(up > 0)
{
sb.append(" UP:").append(up / 1E6D);
}
sb.append(" MHZ");
return sb.toString();
}
}

View File

@@ -0,0 +1,74 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Bandwidth;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* NXDN channel that uses Direct Frequency Assignment.
*/
public class NXDNChannelDFA extends NXDNChannel
{
private ChannelAccessInformation mChannelAccessInformation;
private final int mOutboundChannelNumber;
private final int mInboundChannelNumber;
/**
* Constructs an instance
* @param outboundChannelNumber from message
* @param inboundChannelNumber from message
* @param bandwidth from message
*/
public NXDNChannelDFA(int outboundChannelNumber, int inboundChannelNumber, Bandwidth bandwidth)
{
mOutboundChannelNumber = outboundChannelNumber;
mInboundChannelNumber = inboundChannelNumber;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
mChannelAccessInformation = channelAccessInformation;
}
@Override
public long getDownlinkFrequency()
{
if(mChannelAccessInformation != null)
{
return mChannelAccessInformation.getFrequency(mOutboundChannelNumber);
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mChannelAccessInformation != null)
{
return mChannelAccessInformation.getFrequency(mInboundChannelNumber);
}
return 0;
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* Fake channel that uses a talkgroup value for tracking call detection events by frequency, where the talkgroup value
* represents the tracked frequency.
*/
public class NXDNChannelFake extends NXDNChannel
{
private int mTalkgroup;
public NXDNChannelFake(int talkgroup)
{
mTalkgroup = talkgroup;
}
/**
* Always return false to indicate this a a non-value, fake channel.
*/
@Override
public boolean isValid()
{
return false;
}
@Override
public long getDownlinkFrequency()
{
return mTalkgroup;
}
@Override
public long getUplinkFrequency()
{
return 0;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
//No-op.
}
@Override
public String toString()
{
return "UNKNOWN";
}
}

View File

@@ -0,0 +1,103 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* NXDN channel that uses a lookup table for the frequency values.
*/
public class NXDNChannelLookup extends NXDNChannel
{
private ChannelFrequency mChannelFrequency;
private final int mChannelNumber;
/**
* Constructs an instance
* @param channelNumber for this channel referenced against a global system channel lookup table.
*/
public NXDNChannelLookup(int channelNumber)
{
mChannelNumber = channelNumber;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
mChannelFrequency = channelFrequencyMap.get(mChannelNumber);
}
/**
* Channel number
*/
public int getChannelNumber()
{
return mChannelNumber;
}
@Override
public long getDownlinkFrequency()
{
if(mChannelFrequency != null)
{
return mChannelFrequency.getDownlink();
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mChannelFrequency != null)
{
return mChannelFrequency.getUplink();
}
return 0;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(mChannelFrequency != null)
{
sb.append("DN:").append(getDownlinkFrequency() / 1E6D);
long up = getUplinkFrequency();
if(up > 0)
{
sb.append(" UP:").append(up / 1E6D);
}
sb.append(" MHZ");
}
else
{
sb.append("CHANNEL-MODE [" + mChannelNumber + "] IS MISSING CHANNEL:FREQUENCY MAPPING");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,97 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.channel;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.util.Callback;
/**
* JavaFX wrapper for the channel frequency object to make the fields observable.
*/
public class ObservableChannelFrequency
{
private IntegerProperty mChannel = new SimpleIntegerProperty();
private LongProperty mDownlink = new SimpleLongProperty();
private LongProperty mUplink = new SimpleLongProperty();
/**
* Constructs an instance
* @param channelFrequency object with configuration details
*/
public ObservableChannelFrequency(ChannelFrequency channelFrequency)
{
mChannel.set(channelFrequency.getChannel());
mDownlink.set(channelFrequency.getDownlink());
mUplink.set(channelFrequency.getUplink());
}
/**
* Constructs an instance with no values.
*/
public ObservableChannelFrequency()
{
}
/**
* Creates a ChannelFrequency instance from this observable wrapper.
*/
public ChannelFrequency getChannel()
{
return new ChannelFrequency(mChannel.get(), mDownlink.get(), mUplink.get());
}
/**
* Channel number
*/
public IntegerProperty channelProperty()
{
return mChannel;
}
/**
* Downlink frequency in Hertz
* @return
*/
public LongProperty downlinkProperty()
{
return mDownlink;
}
/**
* Uplink frequency in Hertz
*/
public LongProperty uplinkProperty()
{
return mUplink;
}
/**
* Creates an observable property extractor for use with observable lists to detect changes internal to this object.
*/
public static Callback<ObservableChannelFrequency, Observable[]> extractor()
{
return (ObservableChannelFrequency ocf) -> new Observable[] {ocf.channelProperty(), ocf.downlinkProperty(),
ocf.uplinkProperty()};
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nxdn.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringIdentifier;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationCategory;
import io.github.dsheirer.protocol.Protocol;
/**
* Indicates the network location category
*/
public class NXDNCategory extends StringIdentifier
{
public NXDNCategory(String value, IdentifierClass identifierClass, Form form, Role role)
{
super(value, identifierClass, form, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Creates an instance
* @param category for the system/site values
* @return category identifier
*/
public static NXDNCategory create(LocationCategory category)
{
return new NXDNCategory(category.toString(), IdentifierClass.NETWORK, Form.CATEGORY, Role.BROADCAST);
}
}

Some files were not shown because too many files have changed in this diff Show More