mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 10:07:26 -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">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
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.APCO25TalkgroupFormatter;
|
||||||
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
|
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
|
||||||
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
|
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.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
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.
|
* 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.APCO25, new APCO25TalkgroupFormatter());
|
||||||
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
|
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
|
||||||
|
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
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)"),
|
"<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("*****", 1, 0xFFFF, "1 to 65,535",
|
||||||
"NBFM valid value range is 1-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("*****", 1, 0xFFFF, "1 to 65,535",
|
||||||
"Passport valid value range is 1-65,535"),
|
"Passport valid value range is 1-65,535"),
|
||||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||||
@@ -139,6 +138,8 @@ public enum TalkgroupFormat
|
|||||||
return MPT1327;
|
return MPT1327;
|
||||||
case NBFM:
|
case NBFM:
|
||||||
return NBFM;
|
return NBFM;
|
||||||
|
case NXDN:
|
||||||
|
return NXDN;
|
||||||
case PASSPORT:
|
case PASSPORT:
|
||||||
return PASSPORT;
|
return PASSPORT;
|
||||||
default:
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.LTRTalkgroupFormatter;
|
||||||
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
|
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
|
||||||
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
|
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.PassportTalkgroupFormatter;
|
||||||
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
@@ -59,6 +60,7 @@ public class TalkgroupFormatter
|
|||||||
mFormatterMap.put(Protocol.LTR_NET, ltr);
|
mFormatterMap.put(Protocol.LTR_NET, ltr);
|
||||||
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
|
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
|
||||||
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
|
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
|
||||||
|
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||||
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
|
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
|
||||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 com.fasterxml.jackson.annotation.JsonRootName;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.encryption.EncryptionKey;
|
import io.github.dsheirer.identifier.encryption.EncryptionKey;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -288,10 +287,18 @@ public class MBECallSequence
|
|||||||
}
|
}
|
||||||
else
|
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
|
* 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)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.identifier.IdentifierUpdateNotification;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
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.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.radio.APCO25RadioIdentifier;
|
||||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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 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;
|
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.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
* it under the terms of the GNU General Public License as published by
|
||||||
* later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
* 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.
|
* You should have received a copy of the GNU General Public License
|
||||||
* If not, see <http://www.gnu.org/licenses/>
|
* 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.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
@@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
|
|||||||
*/
|
*/
|
||||||
@JsonRootName(value = "frame")
|
@JsonRootName(value = "frame")
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@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
|
public class VoiceFrame
|
||||||
{
|
{
|
||||||
private long mTimestamp;
|
private long mTimestamp;
|
||||||
@@ -35,6 +38,7 @@ public class VoiceFrame
|
|||||||
private Integer mAlgorithm;
|
private Integer mAlgorithm;
|
||||||
private Integer mKeyId;
|
private Integer mKeyId;
|
||||||
private String mMessageIndicator;
|
private String mMessageIndicator;
|
||||||
|
private String mTag;
|
||||||
|
|
||||||
public VoiceFrame()
|
public VoiceFrame()
|
||||||
{
|
{
|
||||||
@@ -70,6 +74,24 @@ public class VoiceFrame
|
|||||||
mMessageIndicator = messageIndicator;
|
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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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()
|
public String toString()
|
||||||
|
{
|
||||||
|
return toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary representation of the message
|
||||||
|
* @return message in binary 0/1 format
|
||||||
|
*/
|
||||||
|
public String toBinaryString()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
@@ -317,6 +326,10 @@ public class BinaryMessage extends BitSet
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hex string representation of the message
|
||||||
|
* @return message in hex
|
||||||
|
*/
|
||||||
public String toHexString()
|
public String toHexString()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@@ -997,6 +1010,18 @@ public class BinaryMessage extends BitSet
|
|||||||
return String.format("%0" + width + "X", getInt(field));
|
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
|
* 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
|
* 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");
|
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);
|
return message;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.bits;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
|
import java.nio.ByteOrder;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
|
|
||||||
public class CorrectedBinaryMessage extends BinaryMessage
|
public class CorrectedBinaryMessage extends BinaryMessage
|
||||||
@@ -54,6 +55,45 @@ public class CorrectedBinaryMessage extends BinaryMessage
|
|||||||
this.xor(message);
|
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
|
@Override
|
||||||
public int getCorrectedBitCount()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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)
|
* @param start index (inclusive)
|
||||||
* @return constructed bit field.
|
* @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)
|
* @param start index (inclusive)
|
||||||
* @return constructed bit field.
|
* @return constructed bit field.
|
||||||
*/
|
*/
|
||||||
@@ -79,6 +109,16 @@ public record IntField(int start, int end)
|
|||||||
return new IntField(start, (start + 5));
|
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.
|
* Utility constructor method for a field with one octet of length.
|
||||||
* @param start index (inclusive)
|
* @param start index (inclusive)
|
||||||
@@ -89,6 +129,36 @@ public record IntField(int start, int end)
|
|||||||
return new IntField(start, (start + 7));
|
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.
|
* Utility constructor method for a field with 12 bits of length.
|
||||||
* @param start index (inclusive)
|
* @param start index (inclusive)
|
||||||
@@ -99,6 +169,26 @@ public record IntField(int start, int end)
|
|||||||
return new IntField(start, (start + 11));
|
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.
|
* Utility constructor method for a field with two octets of length.
|
||||||
* @param start index (inclusive)
|
* @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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.
|
* Utility constructor method.
|
||||||
* @param start index (inclusive)
|
* @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)
|
* @param start index (inclusive)
|
||||||
* @return constructed bit field.
|
* @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)
|
* @param start index (inclusive)
|
||||||
* @return constructed bit field.
|
* @return constructed bit field.
|
||||||
*/
|
*/
|
||||||
@@ -119,4 +138,24 @@ public record LongField(int start, int end)
|
|||||||
{
|
{
|
||||||
return new LongField(start, (start + 31));
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -98,6 +98,33 @@ public class FloatCircularBuffer
|
|||||||
return values;
|
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
|
* Places the new value into the buffer, overwriting the oldest value
|
||||||
* @param newValue to add to the buffer
|
* @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-2025 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.channel;
|
package io.github.dsheirer.channel;
|
||||||
|
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel descriptor
|
|
||||||
*/
|
|
||||||
public interface IChannelDescriptor
|
public interface IChannelDescriptor
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Downlink frequency for the channel
|
* Downlink frequency for the channel
|
||||||
|
*
|
||||||
* @return frequency in hertz
|
* @return frequency in hertz
|
||||||
*/
|
*/
|
||||||
long getDownlinkFrequency();
|
long getDownlinkFrequency();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uplink frequency for the channel
|
* Uplink frequency for the channel
|
||||||
|
*
|
||||||
* @return frequency in hertz
|
* @return frequency in hertz
|
||||||
*/
|
*/
|
||||||
long getUplinkFrequency();
|
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;
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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));
|
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;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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(
|
mIdentifierUpdateNotificationProxy.receive(new IdentifierUpdateNotification(
|
||||||
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, 0));
|
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, 0));
|
||||||
break;
|
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);
|
mSquelchController.setSquelchLock(true);
|
||||||
break;
|
break;
|
||||||
case REQUEST_CHANGE_CALL_TIMEOUT:
|
case REQUEST_CHANGE_CALL_TIMEOUT:
|
||||||
if(event instanceof ChangeChannelTimeoutEvent)
|
if(event instanceof ChangeChannelTimeoutEvent timeout)
|
||||||
{
|
{
|
||||||
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
|
|
||||||
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
|
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
|
||||||
}
|
}
|
||||||
case CONTINUATION:
|
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
|
* 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.
|
* 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;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.channel.state;
|
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.Form;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.IdentifierClass;
|
import io.github.dsheirer.identifier.IdentifierClass;
|
||||||
@@ -89,9 +89,9 @@ public abstract class TimeslotDecoderState extends DecoderState
|
|||||||
{
|
{
|
||||||
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifier).getValue());
|
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)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.controller.channel.event;
|
package io.github.dsheirer.controller.channel.event;
|
||||||
|
|
||||||
|
import io.github.dsheirer.channel.IBandChannelDescriptor;
|
||||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||||
import io.github.dsheirer.controller.channel.Channel;
|
import io.github.dsheirer.controller.channel.Channel;
|
||||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||||
import io.github.dsheirer.module.ModuleEventBusMessage;
|
import io.github.dsheirer.module.ModuleEventBusMessage;
|
||||||
import io.github.dsheirer.module.decode.event.DecodeEventHistory;
|
import io.github.dsheirer.module.decode.event.DecodeEventHistory;
|
||||||
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
|
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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 channelDescriptor that identifies which channel in a multi-channel config to start
|
||||||
* @param identifierCollection to use for the new channel
|
* @param identifierCollection to use for the new channel
|
||||||
*/
|
*/
|
||||||
public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor,
|
public ChannelStartProcessingRequest(Channel channel, IBandChannelDescriptor channelDescriptor,
|
||||||
IdentifierCollection identifierCollection)
|
IdentifierCollection identifierCollection)
|
||||||
{
|
{
|
||||||
this(channel, channelDescriptor, identifierCollection, null);
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -963,11 +963,11 @@ public class FilterFactory
|
|||||||
* @return filter coefficients.
|
* @return filter coefficients.
|
||||||
* @throws FilterDesignException if the requested length is not odd
|
* @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)
|
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];
|
float[] coefficients = new float[length];
|
||||||
@@ -983,7 +983,46 @@ public class FilterFactory
|
|||||||
for(int x = 1; x <= half; x++)
|
for(int x = 1; x <= half; x++)
|
||||||
{
|
{
|
||||||
double a = piScalor * 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];
|
coefficient *= window[half + x];
|
||||||
coefficients[half + x] = (float)coefficient;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
package io.github.dsheirer.dsp.filter.channelizer;
|
package io.github.dsheirer.dsp.filter.channelizer;
|
||||||
|
|
||||||
import io.github.dsheirer.buffer.INativeBuffer;
|
import io.github.dsheirer.buffer.INativeBuffer;
|
||||||
import io.github.dsheirer.buffer.INativeBufferProvider;
|
|
||||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
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 static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
|
||||||
|
|
||||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||||
private INativeBufferProvider mNativeBufferProvider;
|
private TunerController mTunerController;
|
||||||
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||||
private ChannelCalculator mChannelCalculator;
|
private ChannelCalculator mChannelCalculator;
|
||||||
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
|
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
|
||||||
@@ -85,23 +84,20 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
private boolean mRunning = true;
|
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
|
* @param tunerController for a tuner that provides a baseband complex buffer stream.
|
||||||
* streams
|
|
||||||
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
|
|
||||||
* @param sampleRate of the baseband complex buffer sample 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");
|
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
|
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
|
||||||
if(channelCount % 2 != 0)
|
if(channelCount % 2 != 0)
|
||||||
@@ -109,21 +105,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
channelCount--;
|
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 = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
|
||||||
mBufferDispatcher.setListener(mNativeBufferReceiver);
|
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.
|
* Provides a description of the state of this manager.
|
||||||
*/
|
*/
|
||||||
@@ -201,7 +188,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
||||||
mChannelSourceEventListener, threadName);
|
mChannelSourceEventListener, threadName, mTunerController.getTunerFrequencyErrorManager());
|
||||||
|
|
||||||
mChannelSources.add(channelSource);
|
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 this is the first channel, register to start the sample buffers flowing
|
||||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
|
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
|
||||||
{
|
{
|
||||||
mNativeBufferProvider.addBufferListener(mBufferDispatcher);
|
mTunerController.addBufferListener(mBufferDispatcher);
|
||||||
mPolyphaseChannelizer.start();
|
mPolyphaseChannelizer.start();
|
||||||
mBufferDispatcher.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 this is the last/only channel, deregister to stop the sample buffers
|
||||||
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||||
{
|
{
|
||||||
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
|
mTunerController.removeBufferListener(mBufferDispatcher);
|
||||||
mBufferDispatcher.stop();
|
mBufferDispatcher.stop();
|
||||||
mPolyphaseChannelizer.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_LOCKED:
|
||||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
||||||
case NOTIFICATION_RECORDING_FILE_LOADED:
|
case NOTIFICATION_RECORDING_FILE_LOADED:
|
||||||
|
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||||
//no-op
|
//no-op
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||||
@@ -456,9 +444,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
|
case REQUEST_FREQUENCY_CORRECTION:
|
||||||
//Rebroadcast so that the tuner source can process this event
|
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||||
mSourceEventBroadcaster.broadcast(sourceEvent);
|
//Ignore
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.SourceEvent;
|
||||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||||
|
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -47,6 +48,8 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
|||||||
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
|
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
|
||||||
private double mTunerSampleRate;
|
private double mTunerSampleRate;
|
||||||
private double mTunerCenterFrequency;
|
private double mTunerCenterFrequency;
|
||||||
|
private long mFrequencyCorrection;
|
||||||
|
private boolean mPendingFrequencyCorrectionUpdate = false;
|
||||||
private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate;
|
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 filterManager for access to new or cached synthesis filters
|
||||||
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
||||||
* @param threadName for the channel's dispatcher
|
* @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
|
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
|
||||||
*/
|
*/
|
||||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
|
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator,
|
||||||
Listener<SourceEvent> producerSourceEventListener, String threadName)
|
SynthesisFilterManager filterManager,
|
||||||
|
Listener<SourceEvent> producerSourceEventListener, String threadName,
|
||||||
|
TunerFrequencyErrorManager tunerFrequencyErrorManager)
|
||||||
throws IllegalArgumentException
|
throws IllegalArgumentException
|
||||||
{
|
{
|
||||||
super(producerSourceEventListener, tunerChannel, threadName);
|
super(producerSourceEventListener, tunerChannel, threadName, tunerFrequencyErrorManager);
|
||||||
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
||||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrequencyCorrection(long correction)
|
||||||
|
{
|
||||||
|
mFrequencyCorrection = correction;
|
||||||
|
mPendingFrequencyCorrectionUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description of the state or configuration of the output processor.
|
* Description of the state or configuration of the output processor.
|
||||||
*/
|
*/
|
||||||
@@ -280,6 +293,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
|||||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Apply channel decider requested frequency correction
|
||||||
|
if(mPendingFrequencyCorrectionUpdate)
|
||||||
|
{
|
||||||
|
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||||
|
mPendingFrequencyCorrectionUpdate = false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(mPolyphaseChannelOutputProcessor != null)
|
if(mPolyphaseChannelOutputProcessor != null)
|
||||||
@@ -349,7 +369,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
|||||||
*/
|
*/
|
||||||
public long getFrequencyOffset()
|
public long getFrequencyOffset()
|
||||||
{
|
{
|
||||||
return mIndexCenterFrequency - getTunerChannel().getFrequency();
|
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mFrequencyCorrection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,9 +19,6 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.dsp.filter.design;
|
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.application.Application;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -59,32 +56,68 @@ public class FilterViewer extends Application
|
|||||||
*/
|
*/
|
||||||
private float[] getFilter()
|
private float[] getFilter()
|
||||||
{
|
{
|
||||||
int filterLength = 31;
|
// FIRFilterSpecification specification = FIRFilterSpecification
|
||||||
WindowType windowType = WindowType.BLACKMAN_HARRIS_7;
|
// .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
|
float decimatedSampleRate = 12500f;
|
||||||
.lowPassBuilder()
|
int symbolLength = 26;
|
||||||
.sampleRate(12500)
|
float rrcAlpha = 0.2f;
|
||||||
.passBandCutoff(5200)
|
|
||||||
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
|
|
||||||
.stopBandAmplitude(0.0).stopBandStart(6500) //6500
|
|
||||||
.stopBandRipple(0.01).build();
|
|
||||||
|
|
||||||
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
|
float[] taps = { +0.031462429f, +0.031747267f, +0.030401148f, +0.027362877f,
|
||||||
{
|
+0.022653298f, +0.016379869f, +0.008737200f, +0.000003302f,
|
||||||
taps = FilterFactory.getTaps(specification);
|
-0.009468531f, -0.019262057f, -0.028914291f, -0.037935027f,
|
||||||
}
|
-0.045828927f, -0.052119261f, -0.056372283f, -0.058221106f,
|
||||||
catch(Exception fde) //FilterDesignException
|
-0.057387924f, -0.053703443f, -0.047122444f, -0.037734535f,
|
||||||
{
|
-0.025769308f, -0.011595336f, +0.004287292f, +0.021260954f,
|
||||||
System.out.println("Error");
|
+0.038610717f, +0.055550276f, +0.071252765f, +0.084885375f,
|
||||||
}
|
+0.095646450f, +0.102803611f, +0.105731303f, +0.103946126f,
|
||||||
|
+0.097138329f, +0.085197939f, +0.068234131f, +0.046586711f,
|
||||||
// float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType);
|
+0.020828821f, -0.008239664f, -0.039608255f, -0.072081234f,
|
||||||
// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
|
-0.104311776f, -0.134843790f, -0.162160200f, -0.184736015f,
|
||||||
|
-0.201094346f, -0.209863285f, -0.209831516f, -0.200000470f,
|
||||||
// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
-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)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -52,6 +52,44 @@ public enum Dibit
|
|||||||
mPhase = phase;
|
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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.
|
* Adjusts the pointer to correct a dibit stuff/delete.
|
||||||
* @param offset to adjust.
|
* @param offset to adjust.
|
||||||
@@ -110,7 +122,7 @@ public class DibitDelayLine
|
|||||||
public void log()
|
public void log()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Pointer [").append(mPointer).append("] ");
|
||||||
int pointer = mPointer;
|
int pointer = mPointer;
|
||||||
for(int i = 0; i < mDelayLine.length; i++)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -29,8 +30,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*/
|
*/
|
||||||
public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferProvider
|
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 ByteBuffer mCurrentBuffer;
|
||||||
private int mBufferSize;
|
private int mBufferSize;
|
||||||
private byte mCurrentByte;
|
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.
|
* the buffer pointer to zero so that new dibits can be loaded.
|
||||||
*/
|
*/
|
||||||
private void getNextBuffer()
|
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)
|
if(mCurrentBuffer != null && mBufferListener != null)
|
||||||
{
|
{
|
||||||
mCurrentBuffer.flip();
|
mCurrentBuffer.flip();
|
||||||
mBufferListener.receive(mCurrentBuffer);
|
mBufferListener.receive(mCurrentBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
mCurrentBuffer = ByteBuffer.allocate(mBufferSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -88,17 +95,33 @@ public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferP
|
|||||||
|
|
||||||
if(mDibitCount >= 4)
|
if(mDibitCount >= 4)
|
||||||
{
|
{
|
||||||
mCurrentBuffer.put(mCurrentByte);
|
|
||||||
mCurrentByte = 0x00;
|
|
||||||
mDibitCount = 0;
|
|
||||||
|
|
||||||
if(!mCurrentBuffer.hasRemaining())
|
if(!mCurrentBuffer.hasRemaining())
|
||||||
{
|
{
|
||||||
getNextBuffer();
|
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.
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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);
|
BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
|
||||||
polySet.load(i, crcSize + 1, polynomial);
|
polySet.load(i, crcSize + 1, polynomial);
|
||||||
message.xor(polySet);
|
message.xor(polySet);
|
||||||
System.out.println(message.toString());
|
// System.out.println(message.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
@@ -183,29 +183,29 @@ public class CRCUtil
|
|||||||
|
|
||||||
if(!integerArray)
|
if(!integerArray)
|
||||||
{
|
{
|
||||||
element.append("l");
|
element.append("L");
|
||||||
}
|
}
|
||||||
|
|
||||||
element.append(", ");
|
element.append(", ");
|
||||||
|
|
||||||
if(row.length() + element.length() <= 80)
|
if(row.length() + element.length() <= 120)
|
||||||
{
|
{
|
||||||
row.append(element.toString());
|
row.append(element);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sb.append(row.toString());
|
sb.append(row);
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
row = new StringBuilder();
|
row = new StringBuilder();
|
||||||
row.append(" ");
|
row.append(" ");
|
||||||
|
|
||||||
row.append(element.toString());
|
row.append(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(row.length() > 4)
|
if(row.length() > 4)
|
||||||
{
|
{
|
||||||
sb.append(row.toString());
|
sb.append(row);
|
||||||
sb.append("\n");
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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][]"));
|
setLayout(new MigLayout("", "[grow,fill]", "[grow,fill][]"));
|
||||||
mEditorPanel = new FilterEditorPanel<>(filterSet);
|
mEditorPanel = new FilterEditorPanel<>(filterSet);
|
||||||
JScrollPane scroller = new JScrollPane(mEditorPanel);
|
JScrollPane scroller = new JScrollPane(mEditorPanel);
|
||||||
|
scroller.getVerticalScrollBar().setUnitIncrement(16);
|
||||||
scroller.setViewportView(mEditorPanel);
|
scroller.setViewportView(mEditorPanel);
|
||||||
add(scroller, "wrap");
|
add(scroller, "wrap");
|
||||||
JButton close = new JButton("Close");
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.Listener;
|
||||||
import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule;
|
import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
|
import io.github.dsheirer.source.ChannelFrequencyCorrectionStatusNotification;
|
||||||
import io.github.dsheirer.source.Source;
|
import io.github.dsheirer.source.Source;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource;
|
import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource;
|
||||||
@@ -62,6 +63,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JLayeredPane;
|
import javax.swing.JLayeredPane;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.JSpinner;
|
||||||
import javax.swing.JSplitPane;
|
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 Logger LOGGER = LoggerFactory.getLogger(ChannelSpectrumPanel.class);
|
||||||
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
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 final PlaylistManager mPlaylistManager;
|
||||||
private ProcessingChain mProcessingChain;
|
private ProcessingChain mProcessingChain;
|
||||||
private final ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
|
private final ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
|
||||||
@@ -83,7 +90,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
private final FrequencyOverlayPanel mFrequencyOverlayPanel;
|
private final FrequencyOverlayPanel mFrequencyOverlayPanel;
|
||||||
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||||
private final SpinnerNumberModel mNoiseFloorSpinnerModel;
|
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 mPanelVisible = false;
|
||||||
private boolean mDftProcessing = false;
|
private boolean mDftProcessing = false;
|
||||||
private final NoiseSquelchView mNoiseSquelchView;
|
private final NoiseSquelchView mNoiseSquelchView;
|
||||||
@@ -92,6 +103,9 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
private final JFXPanel mNoiseSquelchPanel;
|
private final JFXPanel mNoiseSquelchPanel;
|
||||||
private final JFXPanel mSymbolPanel;
|
private final JFXPanel mSymbolPanel;
|
||||||
private JSplitPane mSplitPane;
|
private JSplitPane mSplitPane;
|
||||||
|
private JButton mAButton;
|
||||||
|
private JButton mBButton;
|
||||||
|
private FeedbackDecoder mFeedbackDecoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance.
|
* 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]"));
|
fftPanel.setLayout(new MigLayout("insets 0", "[grow,fill]", "[][grow,fill]"));
|
||||||
|
|
||||||
JPanel labelPanel = new JPanel();
|
JPanel labelPanel = new JPanel();
|
||||||
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][grow,fill,left][right][][]", ""));
|
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][][][][][grow,fill][][]", ""));
|
||||||
labelPanel.add(new JLabel("Channel Spectrum "));
|
|
||||||
|
|
||||||
mEstimatedCarrierOffsetFrequencyValueLabel = new JLabel(getPaddedCarrierOffsetLabel(0));
|
mTunerPPMLabel = new JLabel(LABEL_PREFIX_PPM);
|
||||||
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
|
mTunerPPMLabel.setEnabled(false);
|
||||||
labelPanel.add(mEstimatedCarrierOffsetFrequencyValueLabel);
|
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 = new SpinnerNumberModel(18, 8, 36, 1);
|
||||||
mNoiseFloorSpinnerModel.addChangeListener(e -> {
|
mNoiseFloorSpinnerModel.addChangeListener(e -> {
|
||||||
@@ -120,8 +173,8 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
mSpectrumPanel.setSampleSize(number.doubleValue());
|
mSpectrumPanel.setSampleSize(number.doubleValue());
|
||||||
});
|
});
|
||||||
JSpinner noiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
|
JSpinner noiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
|
||||||
|
labelPanel.add(new JLabel("Noise Floor:"));
|
||||||
labelPanel.add(noiseFloorSpinner);
|
labelPanel.add(noiseFloorSpinner);
|
||||||
labelPanel.add(new JLabel("Noise Floor"));
|
|
||||||
|
|
||||||
JButton logIndexesButton = new JButton("Log Settings");
|
JButton logIndexesButton = new JButton("Log Settings");
|
||||||
logIndexesButton.addActionListener(e -> {
|
logIndexesButton.addActionListener(e -> {
|
||||||
@@ -229,11 +282,15 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
mSpectrumPanel.clearSpectrum();
|
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);
|
return prefix + StringUtils.leftPad(String.valueOf(value), 7);
|
||||||
String paddedText = StringUtils.rightPad(paddedValue + " Hz", 20);
|
|
||||||
return "Carrier Offset: " + paddedText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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)
|
private void broadcast(SourceEvent sourceEvent)
|
||||||
{
|
{
|
||||||
if(mProcessingChain != null)
|
if(mProcessingChain != null)
|
||||||
@@ -327,8 +368,14 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
*/
|
*/
|
||||||
private void reset()
|
private void reset()
|
||||||
{
|
{
|
||||||
mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(0));
|
mTunerPPMLabel.setText(LABEL_PREFIX_PPM);
|
||||||
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
|
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.frequencyChange(null, 0));
|
||||||
mFrequencyOverlayPanel.process(SourceEvent.sampleRateChange(0));
|
mFrequencyOverlayPanel.process(SourceEvent.sampleRateChange(0));
|
||||||
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(0);
|
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(0);
|
||||||
@@ -350,6 +397,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
mSymbolView.setProtocol("");
|
mSymbolView.setProtocol("");
|
||||||
mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
|
mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
|
||||||
mProcessingChain.removeModule(mSampleStreamTapModule);
|
mProcessingChain.removeModule(mSampleStreamTapModule);
|
||||||
|
mFeedbackDecoder = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Invoking reset - we're on the Swing dispatch thread here
|
//Invoking reset - we're on the Swing dispatch thread here
|
||||||
@@ -377,6 +425,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
setRightComponent(mSymbolPanel);
|
setRightComponent(mSymbolPanel);
|
||||||
mSymbolView.setSymbolProvider(feedbackDecoder);
|
mSymbolView.setSymbolProvider(feedbackDecoder);
|
||||||
mSymbolView.setProtocol(feedbackDecoder.getProtocolDescription());
|
mSymbolView.setProtocol(feedbackDecoder.getProtocolDescription());
|
||||||
|
mFeedbackDecoder = feedbackDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
mProcessingChain.addModule(mSampleStreamTapModule);
|
mProcessingChain.addModule(mSampleStreamTapModule);
|
||||||
@@ -428,11 +477,38 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
|||||||
@Override
|
@Override
|
||||||
public void receive(SourceEvent sourceEvent)
|
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);
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 AddRadioIdRangeItem(Protocol.DMR));
|
||||||
dmrMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
|
dmrMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
|
||||||
|
|
||||||
|
|
||||||
Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC);
|
Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC);
|
||||||
fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC));
|
fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC));
|
||||||
fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(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 AddTalkgroupRangeItem(Protocol.NBFM));
|
||||||
nbfmMenu.getItems().add(new AddDcsItem());
|
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);
|
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
|
||||||
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
|
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
|
||||||
passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT));
|
passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT));
|
||||||
@@ -779,7 +785,7 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
lojackMenu.getItems().add(new AddLojackItem());
|
lojackMenu.getItems().add(new AddLojackItem());
|
||||||
|
|
||||||
mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
|
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;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.UserPreferences;
|
||||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for talkgroup alias identifiers
|
* Editor for talkgroup alias identifiers
|
||||||
*/
|
*/
|
||||||
@@ -244,6 +243,10 @@ public class RadioIdEditor extends IdentifierEditor<Radio>
|
|||||||
"Format: 0 - 16777215"));
|
"Format: 0 - 16777215"));
|
||||||
mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
||||||
"Format: 0 - FFFFFF"));
|
"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),
|
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
|
||||||
"Format: 0 - 8388607"));
|
"Format: 0 - 8388607"));
|
||||||
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.UserPreferences;
|
||||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for radio ID range alias identifiers
|
* Editor for radio ID range alias identifiers
|
||||||
*/
|
*/
|
||||||
@@ -293,6 +292,10 @@ public class RadioIdRangeEditor extends IdentifierEditor<RadioRange>
|
|||||||
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
|
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
|
||||||
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
||||||
new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF"));
|
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),
|
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
|
||||||
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
|
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
|
||||||
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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"));
|
"Format: 1 - 65535"));
|
||||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
|
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
|
||||||
"Format: 1 - FFFF"));
|
"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),
|
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||||
"Format: 0 - 65535"));
|
"Format: 0 - 65535"));
|
||||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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"));
|
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
|
||||||
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
|
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
|
||||||
new HexFormatter(0,65535), "Format: 0 - FFFF"));
|
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,
|
details.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
|
||||||
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
|
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
|
||||||
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
|
"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"));
|
new IntegerFormatter(0,65535), "Format 0 - 65535"));
|
||||||
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
|
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
|
||||||
new IntegerFormatter(0,65535), "Format 0 - FFFF"));
|
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),
|
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||||
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
|
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
|
||||||
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
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"));
|
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
|
||||||
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
|
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
|
||||||
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
|
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;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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);
|
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case DMR:
|
case DMR:
|
||||||
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case NBFM:
|
|
||||||
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
|
||||||
case LTR_NET:
|
case LTR_NET:
|
||||||
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case LTR:
|
case LTR:
|
||||||
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case MPT1327:
|
case MPT1327:
|
||||||
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
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:
|
case PASSPORT:
|
||||||
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||||
case P25_PHASE1:
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -35,7 +35,7 @@ public enum ModeDecoderType
|
|||||||
DSTAR("d-star", null),
|
DSTAR("d-star", null),
|
||||||
FM("fm", DecoderType.NBFM),
|
FM("fm", DecoderType.NBFM),
|
||||||
FMN("fmn", DecoderType.NBFM),
|
FMN("fmn", DecoderType.NBFM),
|
||||||
NXDN("nxdn", null),
|
NXDN("nxdn", DecoderType.NXDN),
|
||||||
TELM("telm", null),
|
TELM("telm", null),
|
||||||
USB("usb", null),
|
USB("usb", null),
|
||||||
LSB("lsb", 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.identifier.talkgroup.UnknownTalkgroupIdentifier;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
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.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.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||||
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
|
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
@@ -146,10 +149,14 @@ public class RadioReferenceDecoder
|
|||||||
{
|
{
|
||||||
case APCO25:
|
case APCO25:
|
||||||
return APCO25Talkgroup.create(value);
|
return APCO25Talkgroup.create(value);
|
||||||
|
case DMR:
|
||||||
|
return DMRTalkgroup.create(value);
|
||||||
case LTR:
|
case LTR:
|
||||||
return LTRTalkgroup.create(value);
|
return LTRTalkgroup.create(value);
|
||||||
case MPT1327:
|
case MPT1327:
|
||||||
return MPT1327Talkgroup.createTo(value);
|
return MPT1327Talkgroup.createTo(value);
|
||||||
|
case NXDN:
|
||||||
|
return NXDNTalkgroupIdentifier.createTo(value);
|
||||||
case PASSPORT:
|
case PASSPORT:
|
||||||
return PassportTalkgroup.create(value);
|
return PassportTalkgroup.create(value);
|
||||||
default:
|
default:
|
||||||
@@ -385,6 +392,55 @@ public class RadioReferenceDecoder
|
|||||||
return Collections.emptyList();
|
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.
|
* Identifies the sdrtrunk protocol used by the system.
|
||||||
* @param system to identify
|
* @param system to identify
|
||||||
@@ -430,6 +486,7 @@ public class RadioReferenceDecoder
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "NXDN":
|
case "NXDN":
|
||||||
|
return Protocol.NXDN;
|
||||||
case "EDACS":
|
case "EDACS":
|
||||||
case "TETRA":
|
case "TETRA":
|
||||||
case "Midland CMS":
|
case "Midland CMS":
|
||||||
@@ -440,6 +497,8 @@ public class RadioReferenceDecoder
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mLog.info("Type:" + type + " Flavor:" + flavor + " Voice:" + voice);
|
||||||
|
|
||||||
return Protocol.UNKNOWN;
|
return Protocol.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +562,7 @@ public class RadioReferenceDecoder
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "NXDN":
|
case "NXDN":
|
||||||
|
return DecoderType.NXDN;
|
||||||
|
|
||||||
case "EDACS":
|
case "EDACS":
|
||||||
case "TETRA":
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.config.DecodeConfiguration;
|
||||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
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.DecodeConfigP25Phase1;
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.Modulation;
|
import io.github.dsheirer.module.decode.p25.phase1.Modulation;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
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 PRIMARY_CONTROL_CHANNEL = "d";
|
||||||
private static final String TOGGLE_BUTTON_CONTROL = "Control";
|
private static final String TOGGLE_BUTTON_CONTROL = "Control";
|
||||||
private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice";
|
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 static final String PHASE_2_FLAVOR = "Phase II";
|
||||||
|
|
||||||
private UserPreferences mUserPreferences;
|
private UserPreferences mUserPreferences;
|
||||||
@@ -250,6 +252,39 @@ public class SiteEditor extends GridPane
|
|||||||
.getTimeslotFrequencies(systemInformation, site);
|
.getTimeslotFrequencies(systemInformation, site);
|
||||||
dmr.setTimeslotMap(timeslotFrequencies);
|
dmr.setTimeslotMap(timeslotFrequencies);
|
||||||
return dmr;
|
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:
|
case P25_PHASE1:
|
||||||
DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType);
|
DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType);
|
||||||
|
|
||||||
@@ -399,7 +434,7 @@ public class SiteEditor extends GridPane
|
|||||||
getCreateChannelConfigurationButton().setVisible(true);
|
getCreateChannelConfigurationButton().setVisible(true);
|
||||||
getGoToChannelEditorCheckBox().setVisible(true);
|
getGoToChannelEditorCheckBox().setVisible(true);
|
||||||
getSystemTextField().setText(system.getName());
|
getSystemTextField().setText(system.getName());
|
||||||
getSiteTextField().setText(mCurrentSite.getCountyName());
|
getSiteTextField().setText(mCurrentSite.getDescription());
|
||||||
getNameTextField().setText("Control");
|
getNameTextField().setText("Control");
|
||||||
|
|
||||||
if(!siteFrequencies.isEmpty())
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 label = new Label("Please restart the application for this change to take effect");
|
||||||
label.setWrapText(true);
|
label.setWrapText(true);
|
||||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setHeaderText("Restart Required");
|
||||||
alert.getDialogPane().setContent(label);
|
alert.getDialogPane().setContent(label);
|
||||||
alert.initOwner(((Node)getChannelizerTypeChoiceBox()).getScene().getWindow());
|
alert.initOwner(((Node)getChannelizerTypeChoiceBox()).getScene().getWindow());
|
||||||
alert.show();
|
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 label = new Label("Please restart the application for this change to take effect");
|
||||||
label.setWrapText(true);
|
label.setWrapText(true);
|
||||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setHeaderText("Restart Required");
|
||||||
alert.getDialogPane().setContent(label);
|
alert.getDialogPane().setContent(label);
|
||||||
alert.initOwner(((Node)getRspDuoTunerModeChoiceBox()).getScene().getWindow());
|
alert.initOwner(((Node)getRspDuoTunerModeChoiceBox()).getScene().getWindow());
|
||||||
alert.show();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 = new Button("Select ...");
|
||||||
mSelectFileButton.onActionProperty().set(event -> {
|
mSelectFileButton.onActionProperty().set(event -> {
|
||||||
FileChooser fileChooser = new FileChooser();
|
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");
|
fileChooser.setTitle("Select DMR .bits Recording");
|
||||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||||
if(lastDirectory != null)
|
if(lastDirectory != null)
|
||||||
@@ -478,7 +482,6 @@ public class DmrViewer extends VBox
|
|||||||
fileChooser.setInitialDirectory(file);
|
fileChooser.setInitialDirectory(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
|
||||||
File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||||
|
|
||||||
if(selected != null)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 List<DecodeEventSnapshot> mDecodeEvents = new ArrayList<>();
|
||||||
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
|
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
|
||||||
private AudioSegment mAudioSegment;
|
private AudioSegment mAudioSegment;
|
||||||
|
private int mBitCounter;
|
||||||
|
private int mElapsed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
* @param message for this 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;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
public class MessagePackager
|
||||||
{
|
{
|
||||||
private MessagePackage mMessagePackage;
|
private MessagePackage mMessagePackage;
|
||||||
|
private int mBitCounter;
|
||||||
|
private int mLastBitCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* 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.
|
* Adds an audio segment.
|
||||||
* @param audioSegment to add
|
* @param audioSegment to add
|
||||||
@@ -61,7 +72,9 @@ public class MessagePackager
|
|||||||
*/
|
*/
|
||||||
public void add(IMessage message)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.
|
* 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
|
public class MessageRecordingViewer extends VBox
|
||||||
{
|
{
|
||||||
@@ -50,6 +50,7 @@ public class MessageRecordingViewer extends VBox
|
|||||||
private MenuBar mMenuBar;
|
private MenuBar mMenuBar;
|
||||||
private TabPane mTabPane;
|
private TabPane mTabPane;
|
||||||
private int mTabCounterDmr = 1;
|
private int mTabCounterDmr = 1;
|
||||||
|
private int mTabCounterNxdn = 1;
|
||||||
private int mTabCounterP25P1 = 1;
|
private int mTabCounterP25P1 = 1;
|
||||||
private int mTabCounterP25P2 = 1;
|
private int mTabCounterP25P2 = 1;
|
||||||
private UserPreferences mUserPreferences = new UserPreferences();
|
private UserPreferences mUserPreferences = new UserPreferences();
|
||||||
@@ -77,6 +78,12 @@ public class MessageRecordingViewer extends VBox
|
|||||||
getTabPane().getTabs().add(tab);
|
getTabPane().getTabs().add(tab);
|
||||||
getTabPane().getSelectionModel().select(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");
|
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
|
||||||
p25p1MenuItem.onActionProperty().set(event -> {
|
p25p1MenuItem.onActionProperty().set(event -> {
|
||||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences));
|
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences));
|
||||||
@@ -89,7 +96,7 @@ public class MessageRecordingViewer extends VBox
|
|||||||
getTabPane().getTabs().add(tab);
|
getTabPane().getTabs().add(tab);
|
||||||
getTabPane().getSelectionModel().select(tab);
|
getTabPane().getSelectionModel().select(tab);
|
||||||
});
|
});
|
||||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
|
createNewViewerMenu.getItems().addAll(dmrMenuItem, nxdnMenuItem, p25p1MenuItem, p25p2MenuItem);
|
||||||
|
|
||||||
MenuItem exitMenu = new MenuItem("Exit");
|
MenuItem exitMenu = new MenuItem("Exit");
|
||||||
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
|
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
|
||||||
@@ -110,6 +117,7 @@ public class MessageRecordingViewer extends VBox
|
|||||||
mTabPane = new TabPane();
|
mTabPane = new TabPane();
|
||||||
mTabPane.setMaxHeight(Double.MAX_VALUE);
|
mTabPane.setMaxHeight(Double.MAX_VALUE);
|
||||||
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
|
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("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)));
|
||||||
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 = new Button("Select ...");
|
||||||
mSelectFileButton.onActionProperty().set(event -> {
|
mSelectFileButton.onActionProperty().set(event -> {
|
||||||
FileChooser fileChooser = new FileChooser();
|
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");
|
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
|
||||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||||
if(lastDirectory != null)
|
if(lastDirectory != null)
|
||||||
@@ -518,7 +522,6 @@ public class P25P1Viewer extends VBox
|
|||||||
fileChooser.setInitialDirectory(file);
|
fileChooser.setInitialDirectory(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
|
||||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||||
|
|
||||||
if(selected != null)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 = new Button("Select ...");
|
||||||
mSelectFileButton.onActionProperty().set(event -> {
|
mSelectFileButton.onActionProperty().set(event -> {
|
||||||
FileChooser fileChooser = new FileChooser();
|
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");
|
fileChooser.setTitle("Select P25 Phase 2 .bits Recording");
|
||||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||||
if(lastDirectory != null)
|
if(lastDirectory != null)
|
||||||
@@ -604,7 +608,6 @@ public class P25P2Viewer extends VBox
|
|||||||
fileChooser.setInitialDirectory(file);
|
fileChooser.setInitialDirectory(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
|
||||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||||
|
|
||||||
if(selected != null)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -30,6 +30,7 @@ public enum Form
|
|||||||
ARS_USER,
|
ARS_USER,
|
||||||
ARS_PASSWORD,
|
ARS_PASSWORD,
|
||||||
CALL_PROGRESS_TONE,
|
CALL_PROGRESS_TONE,
|
||||||
|
CATEGORY,
|
||||||
CHANNEL,
|
CHANNEL,
|
||||||
CHANNEL_DESCRIPTOR,
|
CHANNEL_DESCRIPTOR,
|
||||||
CHANNEL_NAME,
|
CHANNEL_NAME,
|
||||||
@@ -49,6 +50,7 @@ public enum Form
|
|||||||
NETWORK_ACCESS_CODE,
|
NETWORK_ACCESS_CODE,
|
||||||
PATCH_GROUP,
|
PATCH_GROUP,
|
||||||
RADIO,
|
RADIO,
|
||||||
|
RAN,
|
||||||
RF_SUBSYSTEM,
|
RF_SUBSYSTEM,
|
||||||
SCRAMBLE_PARAMETERS,
|
SCRAMBLE_PARAMETERS,
|
||||||
SHORT_DATA_MESSAGE,
|
SHORT_DATA_MESSAGE,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.identifier.configuration;
|
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.Form;
|
||||||
import io.github.dsheirer.identifier.Identifier;
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
import io.github.dsheirer.identifier.IdentifierClass;
|
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
|
* 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.
|
* 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
|
* Constructs a channel descriptor configuration identifier
|
||||||
*/
|
*/
|
||||||
public ChannelDescriptorConfigurationIdentifier(IChannelDescriptor value)
|
public ChannelDescriptorConfigurationIdentifier(IBandChannelDescriptor value)
|
||||||
{
|
{
|
||||||
super(value, IdentifierClass.CONFIGURATION, Form.CHANNEL_DESCRIPTOR, Role.ANY);
|
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 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);
|
super(value, identifierClass, form, role);
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,7 @@ public class EncryptionKeyIdentifier extends Identifier<EncryptionKey>
|
|||||||
@Override
|
@Override
|
||||||
public Protocol getProtocol()
|
public Protocol getProtocol()
|
||||||
{
|
{
|
||||||
return Protocol.APCO25;
|
return mProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEncrypted()
|
public boolean isEncrypted()
|
||||||
@@ -46,8 +48,8 @@ public class EncryptionKeyIdentifier extends Identifier<EncryptionKey>
|
|||||||
return getValue() != null && getValue().isEncrypted();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.DecodeConfigNBFM;
|
||||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
||||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState;
|
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.P25TrafficChannelManager;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||||
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
|
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
|
||||||
@@ -171,9 +177,6 @@ public class DecoderFactory
|
|||||||
processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
|
processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
|
||||||
trafficChannelManager, channelDescriptor);
|
trafficChannelManager, channelDescriptor);
|
||||||
break;
|
break;
|
||||||
case NBFM:
|
|
||||||
processNBFM(channel, modules, aliasList, decodeConfig);
|
|
||||||
break;
|
|
||||||
case LTR:
|
case LTR:
|
||||||
processLTRStandard(channel, modules, aliasList, (DecodeConfigLTRStandard) decodeConfig);
|
processLTRStandard(channel, modules, aliasList, (DecodeConfigLTRStandard) decodeConfig);
|
||||||
break;
|
break;
|
||||||
@@ -184,6 +187,12 @@ public class DecoderFactory
|
|||||||
processMPT1327(channelMapModel, channel, modules, aliasList, channelType,
|
processMPT1327(channelMapModel, channel, modules, aliasList, channelType,
|
||||||
(DecodeConfigMPT1327) decodeConfig, userPreferences);
|
(DecodeConfigMPT1327) decodeConfig, userPreferences);
|
||||||
break;
|
break;
|
||||||
|
case NBFM:
|
||||||
|
processNBFM(channel, modules, aliasList, decodeConfig);
|
||||||
|
break;
|
||||||
|
case NXDN:
|
||||||
|
processNXDN(channel, userPreferences, modules, aliasList, decodeConfig, trafficChannelManager, channelDescriptor);
|
||||||
|
break;
|
||||||
case PASSPORT:
|
case PASSPORT:
|
||||||
processPassport(channel, modules, aliasList, decodeConfig);
|
processPassport(channel, modules, aliasList, decodeConfig);
|
||||||
break;
|
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.
|
* Creates modules for DMR decoder setup.
|
||||||
*
|
*
|
||||||
@@ -622,9 +680,9 @@ public class DecoderFactory
|
|||||||
|
|
||||||
for(Module module : modules)
|
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:
|
case MPT1327:
|
||||||
filters.add(new MPT1327MessageFilter());
|
filters.add(new MPT1327MessageFilter());
|
||||||
break;
|
break;
|
||||||
|
case NXDN:
|
||||||
|
filters.add(new NXDNMessageFilterSet());
|
||||||
|
break;
|
||||||
case P25_PHASE1:
|
case P25_PHASE1:
|
||||||
filters.add(new P25P1MessageFilterSet());
|
filters.add(new P25P1MessageFilterSet());
|
||||||
break;
|
break;
|
||||||
@@ -708,6 +769,8 @@ public class DecoderFactory
|
|||||||
return new DecodeConfigMPT1327();
|
return new DecodeConfigMPT1327();
|
||||||
case NBFM:
|
case NBFM:
|
||||||
return new DecodeConfigNBFM();
|
return new DecodeConfigNBFM();
|
||||||
|
case NXDN:
|
||||||
|
return new DecodeConfigNXDN();
|
||||||
case PASSPORT:
|
case PASSPORT:
|
||||||
return new DecodeConfigPassport();
|
return new DecodeConfigPassport();
|
||||||
case P25_PHASE1:
|
case P25_PHASE1:
|
||||||
@@ -767,6 +830,13 @@ public class DecoderFactory
|
|||||||
copyNBFM.setSquelchNoiseCloseThreshold(origNBFM.getSquelchNoiseCloseThreshold());
|
copyNBFM.setSquelchNoiseCloseThreshold(origNBFM.getSquelchNoiseCloseThreshold());
|
||||||
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
|
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
|
||||||
return copyNBFM;
|
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:
|
case P25_PHASE1:
|
||||||
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;
|
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;
|
||||||
DecodeConfigP25Phase1 copyP25 = new DecodeConfigP25Phase1();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -28,11 +28,12 @@ public enum DecoderType
|
|||||||
{
|
{
|
||||||
//Primary Decoders
|
//Primary Decoders
|
||||||
AM("AM", "AM", Protocol.UNKNOWN),
|
AM("AM", "AM", Protocol.UNKNOWN),
|
||||||
DMR("DMR","DMR", Protocol.DMR),
|
DMR("DMR", "DMR", Protocol.DMR),
|
||||||
LTR("LTR", "LTR", Protocol.LTR),
|
LTR("LTR", "LTR", Protocol.LTR),
|
||||||
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
|
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
|
||||||
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
|
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
|
||||||
NBFM("NBFM", "NBFM", Protocol.NBFM),
|
NBFM("NBFM", "NBFM", Protocol.NBFM),
|
||||||
|
NXDN("NXDN", "NXDN", Protocol.NXDN),
|
||||||
PASSPORT("Passport", "Passport", Protocol.PASSPORT),
|
PASSPORT("Passport", "Passport", Protocol.PASSPORT),
|
||||||
P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25),
|
P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25),
|
||||||
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
|
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
|
||||||
@@ -44,9 +45,9 @@ public enum DecoderType
|
|||||||
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
|
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
|
||||||
TAIT_1200("Tait 1200", "Tait 1200", Protocol.TAIT1200);
|
TAIT_1200("Tait 1200", "Tait 1200", Protocol.TAIT1200);
|
||||||
|
|
||||||
private String mDisplayString;
|
private final String mDisplayString;
|
||||||
private String mShortDisplayString;
|
private final String mShortDisplayString;
|
||||||
private Protocol mProtocol;
|
private final Protocol mProtocol;
|
||||||
|
|
||||||
DecoderType(String displayString, String shortDisplayString, Protocol protocol)
|
DecoderType(String displayString, String shortDisplayString, Protocol protocol)
|
||||||
{
|
{
|
||||||
@@ -58,38 +59,23 @@ public enum DecoderType
|
|||||||
/**
|
/**
|
||||||
* Primary decoders that operate on I/Q sample streams
|
* Primary decoders that operate on I/Q sample streams
|
||||||
*/
|
*/
|
||||||
public static EnumSet<DecoderType> PRIMARY_DECODERS =
|
public static final EnumSet<DecoderType> PRIMARY_DECODERS = EnumSet.of(AM, DMR, LTR, LTR_NET, MPT1327, NBFM, NXDN,
|
||||||
EnumSet.of(DecoderType.AM,
|
P25_PHASE1, P25_PHASE2, PASSPORT);
|
||||||
DecoderType.DMR,
|
|
||||||
DecoderType.LTR,
|
|
||||||
DecoderType.LTR_NET,
|
|
||||||
DecoderType.MPT1327,
|
|
||||||
DecoderType.NBFM,
|
|
||||||
DecoderType.P25_PHASE1,
|
|
||||||
DecoderType.P25_PHASE2,
|
|
||||||
DecoderType.PASSPORT);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auxiliary decoders that operate on in-band signalling in the decoded audio channel
|
* Auxiliary decoders that operate on in-band signalling in the decoded audio channel
|
||||||
*/
|
*/
|
||||||
public static final EnumSet<DecoderType> AUX_DECODERS =
|
public static final EnumSet<DecoderType> AUX_DECODERS = EnumSet.of(DCS, FLEETSYNC2, LJ_1200, MDC1200, TAIT_1200);
|
||||||
EnumSet.of(DecoderType.DCS,
|
|
||||||
DecoderType.FLEETSYNC2,
|
|
||||||
DecoderType.LJ_1200,
|
|
||||||
DecoderType.MDC1200,
|
|
||||||
DecoderType.TAIT_1200);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decoders that produce a (recordable) bitstream
|
* Decoders that produce a (recordable) bitstream
|
||||||
*/
|
*/
|
||||||
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR,
|
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DMR, MPT1327, NXDN, P25_PHASE1, P25_PHASE2);
|
||||||
DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decoders that produce (recordable) MBE audio codec frames
|
* Decoders that produce (recordable) MBE audio codec frames
|
||||||
*/
|
*/
|
||||||
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS =
|
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS = EnumSet.of(DMR, NXDN, P25_PHASE1, P25_PHASE2);
|
||||||
EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
|
|
||||||
|
|
||||||
public Protocol getProtocol()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceEventProvider
|
||||||
{
|
{
|
||||||
|
private static final double TWO_PI = 2 * Math.PI;
|
||||||
private Listener<SourceEvent> mSourceEventListener;
|
private Listener<SourceEvent> mSourceEventListener;
|
||||||
private Listener<Float> mSymbolListener;
|
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
|
* 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.
|
* Processes the current phase-locked loop (PLL) measurement and sends to the channel frequency error manager to
|
||||||
* @param pllError as measured in the decoder.
|
* 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));
|
long correctionRequestHertz = (long)(mSampleRate / TWO_PI * pllError);
|
||||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(frequencyError, "Decoder measured error sync locked"));
|
broadcast(SourceEvent.frequencyCorrectionRequest(correctionRequestHertz));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,4 +122,14 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE
|
|||||||
mSymbolListener.receive(symbol);
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.module.decode.analog;
|
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.DecoderState;
|
||||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||||
import io.github.dsheirer.channel.state.State;
|
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 final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class);
|
||||||
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
|
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
|
||||||
private DecodeEvent mDecodeEvent;
|
private DecodeEvent mDecodeEvent;
|
||||||
private IChannelDescriptor mChannelDescriptor = null;
|
private IBandChannelDescriptor mChannelDescriptor = null;
|
||||||
|
|
||||||
public AnalogDecoderState()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.ltrstandard.DecodeConfigLTRStandard;
|
||||||
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
||||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
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.p25.phase1.DecodeConfigP25;
|
||||||
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
|
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
|
||||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
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")
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = DecodeConfigAM.class, name = "decodeConfigAM"),
|
@JsonSubTypes.Type(value = DecodeConfigAM.class, name = "decodeConfigAM"),
|
||||||
|
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigLTRNet.class, name = "decodeConfigLTRNet"),
|
@JsonSubTypes.Type(value = DecodeConfigLTRNet.class, name = "decodeConfigLTRNet"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"),
|
@JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"),
|
@JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
|
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
|
||||||
|
@JsonSubTypes.Type(value = DecodeConfigNXDN.class, name = "decodeConfigNXDN"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"),
|
@JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"),
|
||||||
@JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"),
|
@JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport")
|
||||||
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
|
|
||||||
})
|
})
|
||||||
@JacksonXmlRootElement(localName = "decode_configuration")
|
@JacksonXmlRootElement(localName = "decode_configuration")
|
||||||
public abstract class DecodeConfiguration extends Configuration
|
public abstract class DecodeConfiguration extends Configuration
|
||||||
@@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration
|
|||||||
{
|
{
|
||||||
return new int[0];
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.filter.fir.real.RealFIRFilter;
|
||||||
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
|
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
|
||||||
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
|
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.IMessage;
|
||||||
import io.github.dsheirer.message.SyncLossMessage;
|
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.DecoderType;
|
||||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||||
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
|
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.ComplexSamples;
|
||||||
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||||
import io.github.dsheirer.source.ISourceEventListener;
|
import io.github.dsheirer.source.ISourceEventListener;
|
||||||
import io.github.dsheirer.source.ISourceEventProvider;
|
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||||
import java.io.File;
|
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
|
* The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and
|
||||||
* embedded link control parameters.
|
* embedded link control parameters.
|
||||||
*/
|
*/
|
||||||
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
|
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
|
||||||
ISourceEventProvider, Listener<ComplexSamples>
|
ISourceEventListener, Listener<ComplexSamples>
|
||||||
{
|
{
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
|
||||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
|
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
|
||||||
@@ -100,8 +97,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
|
|||||||
private IRealDecimationFilter mDecimationFilterQ;
|
private IRealDecimationFilter mDecimationFilterQ;
|
||||||
private RealFIRFilter mRRCFilterI;
|
private RealFIRFilter mRRCFilterI;
|
||||||
private RealFIRFilter mRRCFilterQ;
|
private RealFIRFilter mRRCFilterQ;
|
||||||
private final PowerMonitor mPowerMonitor = new PowerMonitor();
|
|
||||||
private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
@@ -140,9 +135,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
|
|||||||
SYMBOL_RATE + " symbol rate)");
|
SYMBOL_RATE + " symbol rate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
mPowerMonitor.setSampleRate((int)sampleRate);
|
|
||||||
mCarrierOffsetProcessor.setSampleRate(sampleRate);
|
|
||||||
|
|
||||||
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
||||||
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
|
||||||
|
|
||||||
@@ -158,6 +150,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
|
|||||||
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
|
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
|
||||||
|
|
||||||
float decimatedSampleRate = (float)sampleRate / 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));
|
float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate));
|
||||||
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
|
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
|
||||||
symbolLength += symbolLength % 2; //Make the symbol length even
|
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[] i = mIBasebandFilter.filter(samples.i());
|
||||||
float[] q = mQBasebandFilter.filter(samples.q());
|
float[] q = mQBasebandFilter.filter(samples.q());
|
||||||
|
|
||||||
//Process buffer for power measurements
|
|
||||||
mPowerMonitor.process(i, q);
|
|
||||||
|
|
||||||
i = mDecimationFilterI.decimateReal(i);
|
i = mDecimationFilterI.decimateReal(i);
|
||||||
q = mDecimationFilterQ.decimateReal(q);
|
q = mDecimationFilterQ.decimateReal(q);
|
||||||
|
|
||||||
@@ -201,25 +194,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
|
|||||||
|
|
||||||
float[] demodulated = mDemodulator.demodulate(i, q);
|
float[] demodulated = mDemodulator.demodulate(i, q);
|
||||||
mSymbolProcessor.receive(demodulated);
|
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";
|
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
|
@Override
|
||||||
public void start()
|
public void start()
|
||||||
{
|
{
|
||||||
@@ -340,9 +299,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
|
|||||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||||
setSampleRate(sourceEvent.getValue().doubleValue());
|
setSampleRate(sourceEvent.getValue().doubleValue());
|
||||||
break;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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_PROTECTED_REGION_DIBITS = 92;
|
||||||
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
|
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 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 EQUALIZER_LOOP_GAIN = 0.15f;
|
||||||
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
|
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
|
||||||
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
|
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.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
|
||||||
mEqualizerBalance = Math.max(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
|
//Constrain gain between 1.0f and 1.35f
|
||||||
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
package io.github.dsheirer.module.decode.dmr;
|
package io.github.dsheirer.module.decode.dmr;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
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;
|
||||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||||
import io.github.dsheirer.controller.channel.ChannelConversionRequest;
|
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 currentFrequency of the standard channel
|
||||||
* @param restChannel to identify the new channel frequency to start
|
* @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)
|
DMRNetworkConfigurationMonitor networkConfigurationMonitor)
|
||||||
{
|
{
|
||||||
mLock.lock();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -162,7 +162,7 @@ public class DecodeConfigDMR extends DecodeConfiguration
|
|||||||
@Override
|
@Override
|
||||||
public ChannelSpecification getChannelSpecification()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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;
|
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.Form;
|
||||||
import io.github.dsheirer.identifier.IdentifierClass;
|
import io.github.dsheirer.identifier.IdentifierClass;
|
||||||
import io.github.dsheirer.identifier.Role;
|
import io.github.dsheirer.identifier.Role;
|
||||||
@@ -35,7 +35,7 @@ import org.apache.commons.lang3.Validate;
|
|||||||
*
|
*
|
||||||
* Note: timeslots are tracked as 1 and 2
|
* 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 TimeslotFrequency mTimeslotFrequency;
|
||||||
private int mTimeslot;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.channel.IChannelDescriptor;
|
||||||
import io.github.dsheirer.identifier.Form;
|
import io.github.dsheirer.identifier.Form;
|
||||||
@@ -28,33 +28,33 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic
|
* Base channel event tracker
|
||||||
* channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need
|
* @param <T> type of decode event being tracked.
|
||||||
* for a new event.
|
|
||||||
*/
|
*/
|
||||||
public class P25TrafficChannelEventTracker
|
public abstract class ChannelEventTracker<T extends DecodeEvent>
|
||||||
{
|
{
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelEventTracker.class);
|
||||||
private static final long STALE_EVENT_THRESHOLD_MS = 2000;
|
private static final long STALE_EVENT_THRESHOLD_MS = 3000;
|
||||||
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
|
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
|
||||||
private P25ChannelGrantEvent mEvent;
|
|
||||||
private boolean mStarted = false;
|
private boolean mStarted = false;
|
||||||
private boolean mComplete = false;
|
private boolean mComplete = false;
|
||||||
|
private final T mEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* 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;
|
mEvent = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Access the underlying traffic channel event that is being tracked.
|
* Access the underlying traffic channel event that is being tracked.
|
||||||
|
*
|
||||||
* @return event.
|
* @return event.
|
||||||
*/
|
*/
|
||||||
public P25ChannelGrantEvent getEvent()
|
public T getEvent()
|
||||||
{
|
{
|
||||||
return mEvent;
|
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
|
* 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.
|
* between the event's end time and timestamp argument.
|
||||||
|
*
|
||||||
* @param timestamp to check for staleness
|
* @param timestamp to check for staleness
|
||||||
* @return true if the delta time exceeds a threshold.
|
* @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;
|
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.
|
* Adds the identifier to the tracked event if the event's identifier collection does not already have it.
|
||||||
|
*
|
||||||
* @param identifier to add
|
* @param identifier to add
|
||||||
*/
|
*/
|
||||||
public void addIdentifierIfMissing(Identifier identifier)
|
public void addIdentifierIfMissing(Identifier identifier)
|
||||||
{
|
{
|
||||||
if(identifier != null && !getEvent().getIdentifierCollection().hasIdentifier(identifier))
|
if(identifier != null && !getEvent().getIdentifierCollection().hasIdentifier(identifier))
|
||||||
{
|
{
|
||||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection()
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection().getIdentifiers());
|
||||||
.getIdentifiers());
|
|
||||||
mic.update(identifier);
|
mic.update(identifier);
|
||||||
getEvent().setIdentifierCollection(mic);
|
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
|
* Adds the additional details to the tracked event if the current event details are null or if they do not contain
|
||||||
* the additional details.
|
* the additional details.
|
||||||
* @param additionalDetails
|
*
|
||||||
|
* @param additionalDetails for the event
|
||||||
*/
|
*/
|
||||||
public void addDetailsIfMissing(String additionalDetails)
|
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.
|
* 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.
|
* @param fromToCompare against the current event from identifier.
|
||||||
* @return true if they are different.
|
* @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
|
* Checks the call for staleness and verifies that the call event TO identifier is equal to the TO identifier
|
||||||
* in the provided identifier collection.
|
* in the provided identifier collection.
|
||||||
|
*
|
||||||
* @param toCompareIC containing a TO and optionally a FROM identifier to compare.
|
* @param toCompareIC containing a TO and optionally a FROM identifier to compare.
|
||||||
* @param timestamp to check for staleness
|
* @param timestamp to check for staleness
|
||||||
* @return true if the call identifiers are the same and the call is not stale.
|
* @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.
|
* Indicates if the event has been marked as complete by traffic channel signalling.
|
||||||
|
*
|
||||||
* @return complete status.
|
* @return complete status.
|
||||||
*/
|
*/
|
||||||
public boolean isComplete()
|
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.
|
* Indicates if the event has been marked as started by the traffic channel for HDU or LDU signalling.
|
||||||
|
*
|
||||||
* @return started status.
|
* @return started status.
|
||||||
*/
|
*/
|
||||||
public boolean isStarted()
|
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
|
* Note: once the event is being updated from the traffic channel, any attempts to update from the control channel
|
||||||
* are ignored.
|
* are ignored.
|
||||||
|
*
|
||||||
* @param timestamp to assign.
|
* @param timestamp to assign.
|
||||||
*/
|
*/
|
||||||
public void updateDurationTraffic(long timestamp)
|
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.
|
* Mark the event as complete and assign final end timestamp to the event.
|
||||||
*
|
*
|
||||||
* Note: further attempts to complete an already complete event are ignored.
|
* Note: further attempts to complete an already complete event are ignored.
|
||||||
|
*
|
||||||
* @param timestamp to assign.
|
* @param timestamp to assign.
|
||||||
* @return true if the timestamp was updated
|
* @return true if the timestamp was updated
|
||||||
*/
|
*/
|
||||||
@@ -272,6 +280,7 @@ public class P25TrafficChannelEventTracker
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the details for the tracked event.
|
* Updates the details for the tracked event.
|
||||||
|
*
|
||||||
* @param details to update
|
* @param details to update
|
||||||
*/
|
*/
|
||||||
public void setDetails(String details)
|
public void setDetails(String details)
|
||||||
@@ -279,8 +288,46 @@ public class P25TrafficChannelEventTracker
|
|||||||
getEvent().setDetails(details);
|
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.
|
* Updates the channel descriptor for the tracked event.
|
||||||
|
*
|
||||||
* @param channelDescriptor to update.
|
* @param channelDescriptor to update.
|
||||||
*/
|
*/
|
||||||
public void addChannelDescriptorIfMissing(IChannelDescriptor channelDescriptor)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* @param event for the key
|
||||||
* @return generated key.
|
* @return generated key.
|
||||||
*/
|
*/
|
||||||
private static String getKey(IDecodeEvent event)
|
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();
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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.TableModel;
|
||||||
import javax.swing.table.TableRowSorter;
|
import javax.swing.table.TableRowSorter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode event table view
|
||||||
|
*/
|
||||||
public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain>
|
public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain>
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(DecodeEventPanel.class);
|
private final static Logger mLog = LoggerFactory.getLogger(DecodeEventPanel.class);
|
||||||
private static final String TABLE_PREFERENCE_KEY = "decode.event.panel";
|
private static final String TABLE_PREFERENCE_KEY = "decode.event.panel";
|
||||||
|
|
||||||
private JTable mTable;
|
private final JTable mTable;
|
||||||
private JTableColumnWidthMonitor mTableColumnWidthMonitor;
|
private final DecodeEventModel mEventModel = new DecodeEventModel();
|
||||||
private DecodeEventModel mEventModel = new DecodeEventModel();
|
|
||||||
private DecodeEventHistory mCurrentEventHistory;
|
private DecodeEventHistory mCurrentEventHistory;
|
||||||
private JScrollPane mEmptyScroller;
|
private final IconModel mIconModel;
|
||||||
private IconModel mIconModel;
|
private final AliasModel mAliasModel;
|
||||||
private AliasModel mAliasModel;
|
private final UserPreferences mUserPreferences;
|
||||||
private UserPreferences mUserPreferences;
|
private final TimestampCellRenderer mTimestampCellRenderer;
|
||||||
private TimestampCellRenderer mTimestampCellRenderer;
|
private final FilterSet<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
|
||||||
private FilterSet<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
|
private final HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
|
||||||
private TableRowSorter<TableModel> mTableRowSorter;
|
|
||||||
private HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,19 +93,19 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
mTimestampCellRenderer = new TimestampCellRenderer();
|
mTimestampCellRenderer = new TimestampCellRenderer();
|
||||||
mTable = new JTable(mEventModel);
|
mTable = new JTable(mEventModel);
|
||||||
mTableRowSorter = new TableRowSorter<>(mEventModel);
|
TableRowSorter<TableModel> tableRowSorter = new TableRowSorter<>(mEventModel);
|
||||||
mTableRowSorter.setRowFilter(new EventRowFilter());
|
tableRowSorter.setRowFilter(new EventRowFilter());
|
||||||
mTable.setRowSorter(mTableRowSorter);
|
mTable.setRowSorter(tableRowSorter);
|
||||||
mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
|
JTableColumnWidthMonitor tableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
|
||||||
updateCellRenderers();
|
updateCellRenderers();
|
||||||
mHistoryManagementPanel = new HistoryManagementPanel<>(mEventModel, "Event Filter Editor");
|
mHistoryManagementPanel = new HistoryManagementPanel<>(mEventModel, "Event Filter Editor");
|
||||||
mHistoryManagementPanel.updateFilterSet(mFilterSet);
|
mHistoryManagementPanel.updateFilterSet(mFilterSet);
|
||||||
add(mHistoryManagementPanel, "span,growx");
|
add(mHistoryManagementPanel, "span,growx");
|
||||||
mEmptyScroller = new JScrollPane(mTable);
|
JScrollPane emptyScroller = new JScrollPane(mTable);
|
||||||
add(mEmptyScroller);
|
add(emptyScroller);
|
||||||
|
|
||||||
//Register filter change listener to refresh the table any time the event filters are changed.
|
//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()
|
public void dispose()
|
||||||
@@ -122,7 +122,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
{
|
{
|
||||||
if(preferenceType == PreferenceType.DECODE_EVENT || preferenceType == PreferenceType.TALKGROUP_FORMAT)
|
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)
|
for(Identifier identifier: identifiers)
|
||||||
{
|
{
|
||||||
if(sb.length() > 0)
|
if(!sb.isEmpty())
|
||||||
{
|
{
|
||||||
sb.append(",");
|
sb.append(",");
|
||||||
}
|
}
|
||||||
@@ -262,9 +262,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
ImageIcon icon = null;
|
ImageIcon icon = null;
|
||||||
String text = null;
|
String text = null;
|
||||||
|
|
||||||
if(value instanceof IdentifierCollection)
|
if(value instanceof IdentifierCollection identifierCollection)
|
||||||
{
|
{
|
||||||
IdentifierCollection identifierCollection = (IdentifierCollection)value;
|
|
||||||
List<Identifier> identifiers = identifierCollection.getIdentifiers(mRole);
|
List<Identifier> identifiers = identifierCollection.getIdentifiers(mRole);
|
||||||
|
|
||||||
if(identifiers != null && !identifiers.isEmpty())
|
if(identifiers != null && !identifiers.isEmpty())
|
||||||
@@ -281,13 +280,13 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
|
|
||||||
if(!aliases.isEmpty())
|
if(!aliases.isEmpty())
|
||||||
{
|
{
|
||||||
if(sb.length() > 0)
|
if(!sb.isEmpty())
|
||||||
{
|
{
|
||||||
sb.append(",");
|
sb.append(",");
|
||||||
}
|
}
|
||||||
sb.append(Joiner.on(", ").skipNulls().join(aliases));
|
sb.append(Joiner.on(", ").skipNulls().join(aliases));
|
||||||
color = aliases.get(0).getDisplayColor();
|
color = aliases.getFirst().getDisplayColor();
|
||||||
icon = mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE);
|
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()
|
public DurationCellRenderer()
|
||||||
{
|
{
|
||||||
@@ -372,9 +371,9 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
/**
|
/**
|
||||||
* Frequency value cell renderer
|
* 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()
|
public FrequencyCellRenderer()
|
||||||
{
|
{
|
||||||
@@ -388,10 +387,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
|
|
||||||
String formatted = null;
|
String formatted = null;
|
||||||
|
|
||||||
if(value instanceof IChannelDescriptor)
|
if(value instanceof IChannelDescriptor channelDescriptor)
|
||||||
{
|
{
|
||||||
IChannelDescriptor channelDescriptor = (IChannelDescriptor)value;
|
|
||||||
|
|
||||||
long frequency = channelDescriptor.getDownlinkFrequency();
|
long frequency = channelDescriptor.getDownlinkFrequency();
|
||||||
|
|
||||||
if(frequency > 0)
|
if(frequency > 0)
|
||||||
@@ -409,7 +406,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
|||||||
/**
|
/**
|
||||||
* Channel descriptor value cell renderer
|
* Channel descriptor value cell renderer
|
||||||
*/
|
*/
|
||||||
public class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
|
public static class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
|
||||||
{
|
{
|
||||||
public ChannelDescriptorCellRenderer()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.module.decode.ltrnet;
|
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.DecoderState;
|
||||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||||
@@ -97,7 +97,7 @@ public class LTRNetDecoderState extends DecoderState
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected IChannelDescriptor getCurrentChannel()
|
protected IBandChannelDescriptor getCurrentChannel()
|
||||||
{
|
{
|
||||||
if(mCurrentChannelNumber > 0)
|
if(mCurrentChannelNumber > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.ltrnet.channel;
|
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.module.decode.p25.phase1.message.IFrequencyBand;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
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 mDownlink;
|
||||||
private long mUplink;
|
private long mUplink;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.ltrstandard.channel;
|
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.module.decode.p25.phase1.message.IFrequencyBand;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
public class LtrChannel implements IChannelDescriptor, Comparable<LtrChannel>
|
public class LtrChannel implements IBandChannelDescriptor, Comparable<LtrChannel>
|
||||||
{
|
{
|
||||||
private long mDownlink;
|
private long mDownlink;
|
||||||
private int mChannel;
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.module.decode.mpt1327;
|
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.identifier.IdentifierCollection;
|
||||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
@@ -52,7 +52,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
|
|||||||
protected long mDuration;
|
protected long mDuration;
|
||||||
protected DecodeEventType mDecodeEventType;
|
protected DecodeEventType mDecodeEventType;
|
||||||
protected IdentifierCollection mIdentifierCollection;
|
protected IdentifierCollection mIdentifierCollection;
|
||||||
protected IChannelDescriptor mChannelDescriptor;
|
protected IBandChannelDescriptor mChannelDescriptor;
|
||||||
protected String mDetails;
|
protected String mDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +90,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
|
|||||||
* Sets the channel descriptor for this event
|
* Sets the channel descriptor for this event
|
||||||
* @param channelDescriptor
|
* @param channelDescriptor
|
||||||
*/
|
*/
|
||||||
public MPT1327ChannelGrantDecodeEventBuilder channel(IChannelDescriptor channelDescriptor)
|
public MPT1327ChannelGrantDecodeEventBuilder channel(IBandChannelDescriptor channelDescriptor)
|
||||||
{
|
{
|
||||||
mChannelDescriptor = channelDescriptor;
|
mChannelDescriptor = channelDescriptor;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.mpt1327.channel;
|
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.controller.channel.map.ChannelMap;
|
||||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||||
import io.github.dsheirer.protocol.Protocol;
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MPT-1327 Channel
|
* MPT-1327 Channel
|
||||||
*/
|
*/
|
||||||
public class MPT1327Channel implements IChannelDescriptor
|
public class MPT1327Channel implements IBandChannelDescriptor
|
||||||
{
|
{
|
||||||
private int mChannelNumber;
|
private int mChannelNumber;
|
||||||
private ChannelMap mChannelMap;
|
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