mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
#7 NXDN Decoder WIP
This commit is contained in:
45
.idea/misc.xml
generated
45
.idea/misc.xml
generated
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [" +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
762
src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
Normal file
762
src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()};
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user